From 7ddbd4146b1ffd553ec9affb69d29a11c5508971 Mon Sep 17 00:00:00 2001 From: Nikan Negaresh <84400913+nikan-negaresh-informed@users.noreply.github.com> Date: Sun, 3 Aug 2025 22:53:54 +0100 Subject: [PATCH 1/3] Initial commit (#1) * Initial commit * feat(OSPO): synchronise OSPO workflows * Initial commit --- .gitattributes | 2 + .github/workflows/oss-checker.yml | 124 + .github/workflows/publish-github-release.yml | 117 + .gitignore | 33 + README.md | 459 +++ docker/Dockerfile | 27 + docker/keycloak/README.md | 154 + docker/keycloak/docker-compose.yml | 70 + docker/keycloak/management-node-realm.json | 3348 +++++++++++++++++ docs/JACOCO_COVERAGE.md | 88 + docs/MOCKITO_USAGE.md | 123 + docs/MTLS_CONFIGURATION.md | 160 + docs/entity-dto-converter-pattern.md | 140 + mvnw | 259 ++ mvnw.cmd | 149 + pom.xml | 241 ++ repository-configuration/.gitignore | 3 + repository-configuration/README.md | 32 + repository-configuration/provider.tf | 4 + repository-configuration/repository.tf | 50 + repository-configuration/terraform.tf | 8 + repository-configuration/variables.tf | 28 + .../management/ManagementNodeApplication.java | 13 + .../management/config/ClientIdMdcFilter.java | 99 + .../config/CustomJwtAuthenticationToken.java | 34 + .../KeycloakJwtAuthenticationConverter.java | 337 ++ .../management/config/ModelMapperConfig.java | 27 + .../management/config/SecurityConfig.java | 46 + .../config/SslPropertyInitializer.java | 38 + .../v1/ConfigurationController.java | 53 + .../converter/EntityDtoConverter.java | 59 + .../converter/impl/ConsumerConverter.java | 73 + .../impl/OrganisationProducerConverter.java | 116 + .../converter/impl/ProducerConverter.java | 116 + .../impl/ProductConsumerConverter.java | 60 + .../converter/impl/ProductConverter.java | 73 + .../AuthenticationProcessingException.java | 43 + .../management/exception/ErrorResponse.java | 30 + .../exception/JwtClaimParsingException.java | 30 + .../ResourceAccessParsingException.java | 29 + .../TokenIntrospectionException.java | 30 + .../handlers/GlobalExceptionHandler.java | 101 + .../model/dto/ConsumerConfigDTO.java | 13 + .../management/model/dto/ConsumerDTO.java | 25 + .../model/dto/ProducerConfigDTO.java | 13 + .../management/model/dto/ProducerDTO.java | 35 + .../model/dto/ProductConsumerDTO.java | 25 + .../node/management/model/dto/ProductDTO.java | 28 + .../model/jwt/EnhancedPrincipal.java | 42 + .../node/management/model/jwt/JwtToken.java | 46 + .../persistency/entity/Consumer.java | 32 + .../persistency/entity/Organisation.java | 20 + .../persistency/entity/Producer.java | 47 + .../persistency/entity/Product.java | 36 + .../persistency/entity/ProductConsumer.java | 28 + .../persistency/entity/ProductConsumerId.java | 39 + .../ConsumerProviderRepository.java | 20 + .../repository/ConsumerRepository.java | 18 + .../repository/OrganisationRepository.java | 9 + .../repository/ProducerRepository.java | 18 + .../repository/ProductRepository.java | 19 + .../service/data/ConsumerService.java | 40 + .../service/data/OrganisationService.java | 10 + .../service/data/ProducerService.java | 23 + .../service/data/ProductConsumerService.java | 22 + .../service/data/ProductService.java | 28 + .../data/impl/ConsumerServiceImpl.java | 60 + .../data/impl/OrganisationServiceImpl.java | 25 + .../data/impl/ProducerServiceImpl.java | 48 + .../data/impl/ProductConsumerServiceImpl.java | 49 + .../service/data/impl/ProductServiceImpl.java | 61 + .../configuration/ConfigurationProvider.java | 42 + .../ConfigurationProviderImpl.java | 229 ++ src/main/resources/application.yml | 60 + ...20250728142253__intial_database_tables.sql | 71 + .../samples/V20250728152300__sample_data.sql | 61 + .../ManagementNodeApplicationTests.java | 13 + ...tAuthenticationConverterExceptionTest.java | 196 + ...eycloakJwtAuthenticationConverterTest.java | 412 ++ .../v1/ConfigurationControllerTest.java | 131 + .../converter/impl/ConsumerConverterTest.java | 165 + .../OrganisationProducerConverterTest.java | 413 ++ .../converter/impl/ProducerConverterTest.java | 413 ++ .../impl/ProductConsumerConverterTest.java | 94 + .../converter/impl/ProductConverterTest.java | 165 + ...AuthenticationProcessingExceptionTest.java | 34 + .../exception/SpecificExceptionsTest.java | 89 + .../handlers/GlobalExceptionHandlerTest.java | 107 + ...erProviderOrganisationServiceImplTest.java | 100 + .../data/impl/ConsumerServiceImplTest.java | 172 + .../impl/OrganisationServiceImplTest.java | 33 + .../data/impl/ProducerServiceImplTest.java | 148 + .../impl/ProductConsumerServiceImplTest.java | 150 + .../data/impl/ProductServiceImplTest.java | 195 + .../ConfigurationProviderImplTest.java | 714 ++++ 95 files changed, 12112 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/workflows/oss-checker.yml create mode 100644 .github/workflows/publish-github-release.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docker/Dockerfile create mode 100644 docker/keycloak/README.md create mode 100644 docker/keycloak/docker-compose.yml create mode 100644 docker/keycloak/management-node-realm.json create mode 100644 docs/JACOCO_COVERAGE.md create mode 100644 docs/MOCKITO_USAGE.md create mode 100644 docs/MTLS_CONFIGURATION.md create mode 100644 docs/entity-dto-converter-pattern.md create mode 100755 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 repository-configuration/.gitignore create mode 100644 repository-configuration/README.md create mode 100644 repository-configuration/provider.tf create mode 100644 repository-configuration/repository.tf create mode 100644 repository-configuration/terraform.tf create mode 100644 repository-configuration/variables.tf create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/ManagementNodeApplication.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/ClientIdMdcFilter.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/CustomJwtAuthenticationToken.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/KeycloakJwtAuthenticationConverter.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/ModelMapperConfig.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/SecurityConfig.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/SslPropertyInitializer.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/controller/v1/ConfigurationController.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/EntityDtoConverter.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ConsumerConverter.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/OrganisationProducerConverter.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProducerConverter.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProductConsumerConverter.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProductConverter.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/AuthenticationProcessingException.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/ErrorResponse.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/JwtClaimParsingException.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/ResourceAccessParsingException.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/TokenIntrospectionException.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/handlers/GlobalExceptionHandler.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ConsumerConfigDTO.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ConsumerDTO.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ProducerConfigDTO.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ProducerDTO.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ProductConsumerDTO.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ProductDTO.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/jwt/EnhancedPrincipal.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/jwt/JwtToken.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/Consumer.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/Organisation.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/Producer.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/Product.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/ProductConsumer.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/ProductConsumerId.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ConsumerProviderRepository.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ConsumerRepository.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/OrganisationRepository.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ProducerRepository.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ProductRepository.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/ConsumerService.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/OrganisationService.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/ProducerService.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/ProductConsumerService.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/ProductService.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ConsumerServiceImpl.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/OrganisationServiceImpl.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProducerServiceImpl.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProductConsumerServiceImpl.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProductServiceImpl.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/providers/configuration/ConfigurationProvider.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/providers/configuration/ConfigurationProviderImpl.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/db/migration/V20250728142253__intial_database_tables.sql create mode 100644 src/main/resources/db/samples/V20250728152300__sample_data.sql create mode 100644 src/test/java/uk/gov/dbt/ndtp/ia/node/management/ManagementNodeApplicationTests.java create mode 100644 src/test/java/uk/gov/dbt/ndtp/ia/node/management/config/KeycloakJwtAuthenticationConverterExceptionTest.java create mode 100644 src/test/java/uk/gov/dbt/ndtp/ia/node/management/config/KeycloakJwtAuthenticationConverterTest.java create mode 100644 src/test/java/uk/gov/dbt/ndtp/ia/node/management/controller/v1/ConfigurationControllerTest.java create mode 100644 src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ConsumerConverterTest.java create mode 100644 src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/OrganisationProducerConverterTest.java create mode 100644 src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProducerConverterTest.java create mode 100644 src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProductConsumerConverterTest.java create mode 100644 src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProductConverterTest.java create mode 100644 src/test/java/uk/gov/dbt/ndtp/ia/node/management/exception/AuthenticationProcessingExceptionTest.java create mode 100644 src/test/java/uk/gov/dbt/ndtp/ia/node/management/exception/SpecificExceptionsTest.java create mode 100644 src/test/java/uk/gov/dbt/ndtp/ia/node/management/exception/handlers/GlobalExceptionHandlerTest.java create mode 100644 src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ConsumerProviderOrganisationServiceImplTest.java create mode 100644 src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ConsumerServiceImplTest.java create mode 100644 src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/OrganisationServiceImplTest.java create mode 100644 src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProducerServiceImplTest.java create mode 100644 src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProductConsumerServiceImplTest.java create mode 100644 src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProductServiceImplTest.java create mode 100644 src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/providers/configuration/ConfigurationProviderImplTest.java diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3b41682 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/.github/workflows/oss-checker.yml b/.github/workflows/oss-checker.yml new file mode 100644 index 0000000..2bf4025 --- /dev/null +++ b/.github/workflows/oss-checker.yml @@ -0,0 +1,124 @@ +# SPDX-License-Identifier: Apache-2.0 +# © Crown Copyright 2025. This work has been developed by the National Digital Twin Programme and is legally attributed to the Department for Business and Trade (UK) as the governing entity. + +name: Run OSS check helper + +on: + workflow_dispatch: + +jobs: + oss-checks: + runs-on: ubuntu-latest + + steps: + - name: Fetch GitHub App token for target repo + id: target_token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.OSPO_WORKFLOW_APP_ID }} + private-key: ${{ secrets.OSPO_WORKFLOW_PRIVATE_KEY }} + permission-contents: read + + - name: Fetch GitHub App token for OSPO source repo (read-only) + id: ospo_token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.OSPO_WORKFLOW_APP_ID }} + private-key: ${{ secrets.OSPO_WORKFLOW_PRIVATE_KEY }} + owner: National-Digital-Twin + repositories: ospo-resources + permission-contents: read + + - name: Checkout target repository + uses: actions/checkout@v4 + with: + token: ${{ steps.target_token.outputs.token }} + + - name: Checkout OSPO source repository + uses: actions/checkout@v4 + with: + repository: National-Digital-Twin/ospo-resources + path: ospo-resources + token: ${{ steps.ospo_token.outputs.token }} + + - name: Checkout archetypes source repository + uses: actions/checkout@v4 + with: + repository: National-Digital-Twin/archetypes + path: archetypes + + - name: Test for presence of OSS files and variation from templated content + run: | + missing_files=() + unchanged_files=() + + while IFS= read -r file || [ -n "$file" ]; do + # Skip comments and empty lines + if [[ -z "$file" || "$file" == \#* ]]; then + continue + fi + + target_path="$file" + archetypes_path="archetypes/$file" + + if [ ! -f "$target_path" ]; then + echo "Missing OSS file in target repository: $target_path" + missing_files+=("$file") + elif cmp -s "$target_path" "$archetypes_path"; then + echo "OSS file unchanged from archetypes template: $target_path" + unchanged_files+=("$file") + else + echo "OSS file present and different from the archetypes template: $target_path" + fi + done < ospo-resources/oss-checklist-files.txt + + echo "" + if [ ${#missing_files[@]} -ne 0 ]; then + echo "The following OSS required files are missing:" + printf '%s\n' "${missing_files[@]}" + fi + + if [ ${#unchanged_files[@]} -ne 0 ]; then + echo "The following OSS required files are unchanged from the archetypes template:" + printf '%s\n' "${unchanged_files[@]}" + fi + + if [ ${#missing_files[@]} -ne 0 ] || [ ${#unchanged_files[@]} -ne 0 ]; then + echo "OSS required file check failed." + exit 1 + else + echo "All OSS files are present and have been updated from their original templated content." + fi + + - name: Check GitHub template files are present + run: | + echo "Checking for pull request and issue template files" + + missing_templates=() + + files_to_check=( + ".github/PULL_REQUEST_TEMPLATE.md" + ".github/ISSUE_TEMPLATE/bug_report.md" + ".github/ISSUE_TEMPLATE/feature_request.md" + ) + + for file in "${files_to_check[@]}"; do + if [ ! -f "$file" ]; then + missing_templates+=("$file") + fi + done + + if [ ${#missing_templates[@]} -ne 0 ]; then + echo "" + echo "Required GitHub template files not found:" + printf ' - %s\n' "${missing_templates[@]}" + echo "" + echo "These files help improve project collaboration and are considered best practice." + echo "These need to be included in repository contents to improve the developer and repository consumer experience." + + # Fail the job + echo "Missing required GitHub template files." + exit 1 + else + echo "Required pull request and issue template files present." + fi diff --git a/.github/workflows/publish-github-release.yml b/.github/workflows/publish-github-release.yml new file mode 100644 index 0000000..23a5669 --- /dev/null +++ b/.github/workflows/publish-github-release.yml @@ -0,0 +1,117 @@ +# SPDX-License-Identifier: Apache-2.0 +# © Crown Copyright 2025. This work has been developed by the National Digital Twin Programme and is legally attributed to the Department for Business and Trade (UK) as the governing entity. + +# This workflow is triggered when a pull request is merged into the main branch +# from a release/* branch. It extracts the release version from the source branch, +# generates a Software Bill of Materials (SBOM) using the GitHub API, +# creates a Git tag with the version, and publishes a GitHub release including the SBOM file. + +name: Generate SBOM, Tag and Publish GitHub Release + +on: + pull_request: + types: + - closed + branches: + - main + +permissions: + contents: write + +jobs: + versioning: + if: github.event.pull_request.merged == true && startsWith(github.head_ref, 'release/') + name: Extract Release Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.extract_version.outputs.VERSION }} + steps: + - name: Extract Version from Source Branch Name + id: extract_version + run: | + SOURCE_BRANCH="${{ github.head_ref }}" + VERSION=$(echo "$SOURCE_BRANCH" | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+') + + if [ -z "$VERSION" ]; then + echo "Error: No semantic release version found in source branch: $SOURCE_BRANCH" + exit 1 + fi + + echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + + - name: Validate Version Format (Semantic Versioning) + run: | + if [[ ! "${{ env.VERSION }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: Invalid version format found. Expected semantic version in release branch name (e.g., release/0.9.0)" + exit 1 + fi + + - name: Print Tag Version + run: | + echo "Identified release semantic version: ${{ steps.extract_version.outputs.version }}" + + generate-sbom: + name: Generate SPDX SBOM + runs-on: ubuntu-latest + needs: [versioning] + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Generate SPDX SBOM + run: | + # Call GitHub API to generate SBOM + api_response=$(curl -sSL \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "$GITHUB_API_URL/repos/${{ github.repository }}/dependency-graph/sbom") + + # Extract nested "sbom" object into a valid SPDX file + echo "$api_response" | jq '.sbom' > sbom.spdx.json + + - name: Upload SBOM Artifact + uses: actions/upload-artifact@v4 + with: + name: sbom + path: sbom.spdx.json + + create-git-tag: + name: Create Git Tag + needs: [versioning, generate-sbom] + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Create Git Tag + uses: rickstaa/action-create-tag@v1 + with: + tag: "v${{ needs.versioning.outputs.version }}" + message: "Release v${{ needs.versioning.outputs.version }}" + force_push_tag: true + + create-git-release: + name: Create GitHub Release + needs: [versioning, generate-sbom, create-git-tag] + runs-on: ubuntu-latest + steps: + - name: Download SBOM Artifact + uses: actions/download-artifact@v4 + with: + name: sbom + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: "v${{ needs.versioning.outputs.version }}" + name: "Release v${{ needs.versioning.outputs.version }}" + body: "Automated release for version ${{ needs.versioning.outputs.version }}. For details of fixes, new features and changes in this release, please see [CHANGELOG.md](${{ github.server_url }}/${{ github.repository }}/blob/main/CHANGELOG.md)." + draft: false + prerelease: false + files: | + sbom.spdx.json + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..34fed40 --- /dev/null +++ b/README.md @@ -0,0 +1,459 @@ +# Management Node Module + +## Overview +The Management Node Module is a Spring Boot application that provides APIs to be accessed by Consumer and Producer Federators. It implements a secure communication architecture using Mutual TLS (MTLS) connectivity between Federator instances and itself, as well as establishing zero trust connectivity with Keycloak for authentication and authorization. + +## Prerequisites +- Java 21 +- Maven 3.9+ +- Docker and Docker Compose +- OpenSSL (for certificate generation) + +## Quick Start + +### Setting up Keycloak with Docker Compose + +The application uses Keycloak for authentication and authorization. Follow these steps to set up Keycloak using Docker Compose: + +1. Navigate to the docker directory: + ```bash + cd docker + ``` + +2. Make sure you have the required certificates in place: + - `keystore.jks` - Java keystore containing the server certificate + - `truststore.jks` - Java truststore containing trusted certificates + - `localhost.p12` - PKCS12 keystore for client authentication + - `localhost.crt` - Certificate file + - `localhost.key` - Private key file + + If you need to generate these files for development, see the [Certificate Setup](#certificate-setup) section. + +3. Start Keycloak and PostgreSQL using Docker Compose: + ```bash + docker-compose up -d + ``` + +4. Verify that Keycloak is running: + ```bash + curl -k https://localhost:8443/health + ``` + +5. Access the Keycloak admin console at https://localhost:8443/admin with the following credentials: + - Username: `admin` + - Password: `password` + +### Configuration + +For Docker Compose to run successfully, you need to create a `.env` file in the `docker/keycloak` directory with the following settings: + +``` +POSTGRES_DB=keycloak_db +POSTGRES_USER=keycloak_db_user +POSTGRES_PASSWORD=keycloak_db_user_password +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=password +KC_HOSTNAME_STRICT_BACKCHANNEL=false +SERVER_SSL_KEY_STORE_PASSWORD=changeit +SERVER_SSL_TRUST_STORE_PASSWORD=changeit +KC_HTTPS_KEY_STORE_PASSWORD=changeit +KC_HTTPS_TRUST_STORE_PASSWORD=changeit +KC_SPI_TRUSTSTORE_FILE_PASSWORD=changeit +KC_HOSTNAME=keycloak +KC_HOSTNAME_PORT=8080 +KC_HTTP_ENABLED=false +KC_HOSTNAME_STRICT_HTTPS=false +KC_HEALTH_ENABLED=true +KC_DB=postgres +KC_HTTPS_CLIENT_AUTH=required +KC_HTTPS_ENABLED=true +KC_HTTPS_PORT=8443 +KC_LOG_LEVEL=INFO +``` + +This file contains essential environment variables for both PostgreSQL and Keycloak configuration. You can modify these values as needed for your environment, but make sure to create this file before running Docker Compose. + +## Certificate Setup + +The Management Node Module implements a zero-trust security architecture using Mutual TLS (MTLS) for secure communication between all components. This section explains why certificates are needed, how to generate them, and where they are used in the system. + +> **Note:** For detailed instructions on configuring MTLS for both Keycloak and the Management Node, see the [MTLS Configuration Guide](docs/MTLS_CONFIGURATION.md). + +### Why Certificates Are Needed + +1. **Zero-Trust Security Model**: The system follows a zero-trust approach where all communications must be authenticated and encrypted, regardless of whether they occur inside or outside the network perimeter. + +2. **Mutual TLS (MTLS)**: Unlike standard TLS where only the server authenticates itself to the client, MTLS requires both parties to authenticate each other using X.509 certificates. + +3. **Service-to-Service Authentication**: Certificates provide a secure way for services to verify each other's identity without relying on passwords or API keys. + +### Certificate Types and Their Purpose + +The system requires several certificate files: + +1. **Private Key (`localhost.key`)**: + - The private key used to sign and decrypt data + - Must be kept secure and never shared + - Used by both Keycloak and the Management Node + +2. **Certificate (`localhost.crt`)**: + - The public certificate containing the public key + - Shared with other services to verify the identity + - Used in both server and client authentication + +3. **PKCS12 Keystore (`localhost.p12`)**: + - A container format that stores the private key and certificate + - Used primarily for client authentication + - Imported by Keycloak for client certificate validation + +4. **Java Keystore (`keystore.jks`)**: + - Java-specific format for storing the server's private key and certificate + - Used by both Keycloak and the Management Node for their TLS endpoints + +5. **Java Truststore (`truststore.jks`)**: + - Contains certificates that the server trusts + - Used to validate client certificates during MTLS + +### Step-by-Step Certificate Generation + +For development purposes, follow these steps to generate certificates for mTLS. All passwords used are `changeit`. + +1. **Generate a Root CA certificate**: + ```bash + openssl req -x509 -sha256 -days 3650 -newkey rsa:4096 -keyout rootCA.key -out rootCA.crt + ``` + This creates a Root Certificate Authority (CA) that will be used to sign other certificates. The certificate is valid for 10 years (3650 days). + +2. **Generate a host certificate**: + ```bash + openssl req -new -newkey rsa:4096 -keyout localhost.key -out localhost.csr -nodes + ``` + This creates a private key and certificate signing request (CSR) for the host. + +3. **Sign the host certificate with the Root CA**: + ```bash + openssl x509 -req -CA rootCA.crt -CAkey rootCA.key -in localhost.csr -out localhost.crt -days 365 -CAcreateserial -extfile localhost.ext + ``` + This signs the host CSR with the Root CA, creating a certificate valid for 365 days. + + The content of the `localhost.ext` file should be: + ``` + authorityKeyIdentifier=keyid,issuer + basicConstraints=CA:FALSE + subjectAltName = @alt_names + [alt_names] + DNS.1 = localhost + DNS.2 = keycloak + ``` + This configuration specifies that the certificate is valid for both `localhost` and `keycloak` hostnames. + +4. **Create a PKCS12 keystore for the server**: + ```bash + openssl pkcs12 -export -out localhost.p12 -name "localhost" -inkey localhost.key -in localhost.crt + ``` + This bundles the host certificate and private key into a PKCS12 format. + +5. **Create a PEM file for Linux keystore**: + ```bash + openssl pkcs12 -in localhost.p12 -clcerts -nokeys -out localhost.pem + ``` + This extracts the certificate (without the private key) in PEM format. + +6. **Add the Root CA to the Trust Store**: + ```bash + keytool -importcert -file rootCA.crt -alias clientca -keystore localhost.p12 -storetype PKCS12 -storepass changeit + ``` + This adds the Root CA to the trust store so that clients signed by this CA will be trusted. + +7. **Generate a client certificate**: + ```bash + openssl req -new -newkey rsa:4096 -nodes -keyout client.key -out client.csr + ``` + This creates a private key and CSR for the client. + +8. **Sign the client certificate with the Root CA**: + ```bash + openssl x509 -req -CA rootCA.crt -CAkey rootCA.key -in client.csr -out client.crt -days 365 -CAcreateserial + ``` + This signs the client CSR with the Root CA, creating a certificate valid for 365 days. + +9. **Create a PKCS12 keystore for the client**: + ```bash + openssl pkcs12 -export -out client.p12 -name "client" -inkey client.key -in client.crt + ``` + This bundles the client certificate and private key into a PKCS12 format for use in browsers or client applications. + +10. **Create a Java keystore using keytool**: + ```bash + keytool -importkeystore -destkeystore keystore.jks -srckeystore localhost.p12 -srcstoretype PKCS12 -alias "localhost" + ``` + This converts the PKCS12 keystore to a Java KeyStore (JKS) format used by Java applications. + +11. **Create a Java truststore using keytool**: + ```bash + keytool -import -trustcacerts -noprompt -alias ca -ext san=dns:localhost,ip:127.0.0.1 -file rootCA.crt -keystore truststore.jks + ``` + This creates a truststore containing the Root CA certificate, which will be used to validate client certificates. + +12. **Import the Root CA into the truststore**: + ```bash + keytool -importcert -file rootCA.crt -alias rootCA -keystore truststore.jks -storetype JKS + ``` + This ensures the Root CA is properly imported into the Java truststore. + +13. **Test mTLS connectivity**: + ```bash + curl --location 'https://localhost:8443/realms/management-node/protocol/openid-connect/token' \ + --cert client.crt --key client.key \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'client_id=ztf-client' \ + --data-urlencode 'grant_type=client_credentials' + ``` + This tests the mTLS setup by attempting to obtain a token from Keycloak using client certificate authentication. + +### Certificate Placement and Configuration + +After generating the certificates, place them in the appropriate locations: + +1. **For Keycloak**: + - Place all certificate files in the `docker` directory + - The docker-compose.yml maps these files into the Keycloak container: + ```yaml + volumes: + - ./localhost.p12:/keystores/localhost.p12 + - ./localhost.crt:/cert/localhost.crt + - ./localhost.key:/key/localhost.key + - ./keystore.jks:/cert/keystore.jks + - ./truststore.jks:/cert/truststore.jks + ``` + - Keycloak uses these certificates for: + - Securing its HTTPS endpoint (port 8443) + - Validating client certificates for MTLS + +2. **For Management Node**: + - The application.yml references the certificate files: + ```yaml + server: + ssl: + key-store: /path/to/keystore.jks + key-store-password: changeit + trust-store: /path/to/truststore.jks + trust-store-password: changeit + ``` + - When running in Docker, the Dockerfile copies these files: + ```dockerfile + COPY docker/keystore.jks /app/docker/keystore.jks + COPY docker/truststore.jks /app/docker/truststore.jks + ``` + +3. **For Client Applications**: + - Client applications connecting to the Management Node need: + - The client certificate and private key for authentication + - The server's certificate in their truststore to validate the server + +### Certificate Password Management + +All certificates use the password "changeit" for development. These passwords are configured in the `.env` file: + +``` +SERVER_SSL_KEY_STORE_PASSWORD=changeit +SERVER_SSL_TRUST_STORE_PASSWORD=changeit +KC_HTTPS_KEY_STORE_PASSWORD=changeit +KC_HTTPS_TRUST_STORE_PASSWORD=changeit +KC_SPI_TRUSTSTORE_FILE_PASSWORD=changeit +``` + +For production environments, use strong, unique passwords and secure storage solutions for managing these credentials. + +## Keycloak Realm Setup + +After starting Keycloak, you need to set up a realm for the Management Node. You can either import the pre-configured realm or create it manually. + +### Option 1: Import the Realm Configuration (Recommended) + +1. Log in to the Keycloak admin console at https://localhost:8443/admin +2. Click on the dropdown menu in the top-left corner (it may show "master" if you haven't created any realms yet) +3. Click on "Create Realm" or "Add realm" button +4. Click on the "Browse" or "Select file" button +5. Navigate to and select the `docker/keycloak/management-node-realm.json` file from your project directory +6. Click "Create" or "Import" +7. After the import is complete, verify that the `management-node` realm has been created with all the necessary configurations +8. Note the client secret for the `ztf-client` from the Credentials tab (Clients → ztf-client → Credentials) and update it in your application.yml if needed + +### Option 2: Manual Configuration + +If you prefer to set up the realm manually: + +1. Log in to the Keycloak admin console at https://localhost:8443/admin +2. Create a new realm named `management-node` +3. Create a client with the following settings: + - Client ID: `ztf-client` + - Client Protocol: `openid-connect` + - Access Type: `confidential` + - Valid Redirect URIs: `https://localhost:8090/*` + - Web Origins: `+` +4. Note the client secret from the Credentials tab and update it in your application.yml if needed + +## Building and Running with Maven + +### Building the Application + +The Management Node Module uses Maven for dependency management and build automation. To build the application: + +1. Ensure you have Maven 3.9+ installed: + ```bash + mvn --version + ``` + +2. Build the application: + ```bash + mvn clean package + ``` + This command will: + - Clean the target directory + - Compile the source code + - Run the tests + - Package the application into a JAR file + +3. If you want to skip tests during the build: + ```bash + mvn clean package -DskipTests + ``` + +### Running the Application + +After building, you can run the application using one of these methods: + +1. Using the Java command: + ```bash + java -jar target/management-node-0.0.1.jar + ``` + +2. Using the Maven Spring Boot plugin: + ```bash + mvn spring-boot:run + ``` + +3. Using Docker: + ```bash + docker build -t management-node -f docker/Dockerfile . + docker run -p 8090:8090 management-node + ``` + +The application will be available at https://localhost:8090 + +### Using Profile-Specific Configuration Files + +Spring Boot supports profile-specific property files, which are essential for local development environments where you need to configure sensitive information like passwords and URLs without committing them to version control. + +#### Why Use Profile-Specific Configuration? + +1. **Security**: Keep sensitive information like passwords and API keys out of version control +2. **Environment-Specific Settings**: Configure different settings for development, testing, and production +3. **Local Development**: Each developer can have their own configuration without affecting others + +#### Creating a Profile-Specific YAML File + +1. Create a file named `application-{profile}.yml` in the `src/main/resources` directory, where `{profile}` is the name of your profile (e.g., `application-local.yml` for a "local" profile) + +2. Add your environment-specific configuration to this file. For example: + + ```yaml + spring: + security: + oauth2: + resourceserver: + opaquetoken: + client-secret: your-client-secret-here + client-id: ztf-client + datasource: + password: your-database-password-here + + server: + ssl: + key-store-password: your-keystore-password-here + trust-store-password: your-truststore-password-here + key-store: /path/to/your/local/keystore.jks + trust-store: /path/to/your/local/truststore.jks + ``` + +3. Make sure not to commit this file to version control by adding it to your `.gitignore` file: + ``` + src/main/resources/application-local.yml + ``` + +#### Running the Application with a Specific Profile + +To run the application with your profile, use one of these methods: + +1. Using the Java command with the `spring.profiles.active` parameter: + ```bash + java -jar target/management-node-0.0.1.jar --spring.profiles.active=local + ``` + +2. Using the Maven Spring Boot plugin: + ```bash + mvn spring-boot:run -Dspring-boot.run.profiles=local + ``` + +3. Using environment variables: + ```bash + export SPRING_PROFILES_ACTIVE=local + java -jar target/management-node-0.0.1.jar + ``` + +4. When running with Docker, you can pass the profile as an environment variable: + ```bash + docker run -p 8090:8090 -e "SPRING_PROFILES_ACTIVE=local" management-node + ``` + +The application will load both the default `application.yml` and your profile-specific `application-local.yml`, with the latter overriding any duplicate properties. + +## Code Coverage with JaCoCo + +The project uses JaCoCo for code coverage analysis. For detailed information about the JaCoCo setup, thresholds, and recommendations, see the [JaCoCo Coverage Documentation](docs/JACOCO_COVERAGE.md). + +### Running Code Coverage + +To generate code coverage reports: + +1. Run the Maven verify goal: + ```bash + mvn clean verify + ``` + +2. The JaCoCo report will be generated in the `target/site/jacoco` directory. + +3. Open `target/site/jacoco/index.html` in a web browser to view the detailed coverage report. + +The current configuration aims for 80% code coverage across instructions, branches, lines, methods, and 50% for classes. + +## Troubleshooting + +### Common Issues + +1. **Certificate Issues**: + - Ensure that the paths to the keystore and truststore files in application.yml are correct + - Verify that the certificate passwords match those in the .env file + +2. **Keycloak Connection Issues**: + - Check that Keycloak is running and accessible at https://localhost:8443 + - Verify that the client secret in application.yml matches the one in Keycloak + +3. **Database Connection Issues**: + - Ensure PostgreSQL is running and accessible + - Check the database credentials in the .env file + +## Security Considerations + +This setup implements a zero-trust security model with: +- MTLS for all service-to-service communication +- JWT-based authentication and authorization via Keycloak +- HTTPS for all endpoints +- Client certificate authentication + +For production deployments, consider: +- Using properly signed certificates from a trusted CA +- Implementing network segmentation +- Regularly rotating secrets and certificates +- Setting up monitoring and alerting for security events \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..90d0a7e --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,27 @@ +# Build stage +FROM maven:3.9.6-eclipse-temurin-21-alpine AS build +WORKDIR /build + +# Copy the project files +COPY pom.xml . +COPY src ./src + +# Build the application +RUN mvn clean package -DskipTests + +# Runtime stage +FROM eclipse-temurin:21-jdk-alpine + +WORKDIR /app + +# Create directory for certificates +RUN mkdir -p /app/docker + +# Copy application jar from build stage and certificates +COPY --from=build /build/target/management-node-0.0.1.jar /app/app.jar +COPY docker/keystore.jks /app/docker/keystore.jks +COPY docker/truststore.jks /app/docker/truststore.jks + +EXPOSE 8090 + +ENTRYPOINT ["java", "-jar", "/app/app.jar"] \ No newline at end of file diff --git a/docker/keycloak/README.md b/docker/keycloak/README.md new file mode 100644 index 0000000..c263779 --- /dev/null +++ b/docker/keycloak/README.md @@ -0,0 +1,154 @@ +# mTLS with KeyCloak + +## Create X.509 certificates + + + +All passwords: _changeit_ + +## RootCA + + openssl req -x509 -sha256 -days 3650 -newkey rsa:4096 -keyout rootCA.key -out rootCA.crt + +## Host certificate + + openssl req -new -newkey rsa:4096 -keyout localhost.key -out localhost.csr -nodes + +Sign host csr with rootCA (see below for file `localhost.ext`): + + openssl x509 -req -CA rootCA.crt -CAkey rootCA.key -in localhost.csr -out localhost.crt -days 365 -CAcreateserial -extfile localhost.ext + +### Create pkcs12 file for server +Import local key and crt in keystore to create the "certificate" to be used in keyCloak Server Config: + + openssl pkcs12 -export -out localhost.p12 -name "localhost" -inkey localhost.key -in localhost.crt + +PEM file creation to be used in linux keystore + + openssl pkcs12 -in localhost.p12 -clcerts -nokeys -out localhost.pem + +adding CA Root to Trust Store + + keytool -importcert -file rootCA.crt -alias clientca -keystore localhost.p12 -storetype PKCS12 -storepass changeit + +--- + +## Client (user) certificate + + openssl req -new -newkey rsa:4096 -nodes -keyout client.key -out client.csr + +Sign client csr with rootCA: + + openssl x509 -req -CA rootCA.crt -CAkey rootCA.key -in client.csr -out client.crt -days 365 -CAcreateserial + +Import client key and crt in keystore to create the "certificate" to be used in the browser: + + openssl pkcs12 -export -out client.p12 -name "client" -inkey client.key -in client.crt + + + + +### Create a keystore using keytool + + keytool -importkeystore -destkeystore keystore.jks -srckeystore localhost.p12 -srcstoretype PKCS12 -alias "localhost" + +--- + + +### Create a truststore using keytool + + keytool -import -trustcacerts -noprompt -alias ca -ext san=dns:localhost,ip:127.0.0.1 -file rootCA.crt -keystore truststore.jks + + ##Or in pkcs12 + openssl pkcs12 -export -in rootCA.crt -inkey rootCA.key -out truststore.p12 -name "server certificate" -chain -CAfile rootCA.crt -caname "self signed ca certificate" -passin pass:$PW -passout pass:$PW + +### import the Root CA into TrustStore + + keytool -importcert -file rootCA.crt -alias rootCA -keystore truststore.jks -storetype JKS +--- + + +## To Test MTLS: + + curl --location 'https://localhost:8443/realms/management-node/protocol/openid-connect/token' \ +--cert client.crt --key client.key \ +--header 'Content-Type: application/x-www-form-urlencoded' \ +--data-urlencode 'client_id=ztf-client' \ +--data-urlencode 'grant_type=client_credentials' + +--- + +## Docker Setup + +### Prerequisites +- Docker and Docker Compose installed on your system +- Maven installed for building the application + +### Building the Application +Before running the Docker containers, build the Spring Boot application: + +```bash +cd /path/to/managementNode +mvn clean package +``` + +### Running with Docker Compose +1. Set up environment variables in a `.env` file in the docker directory: + +``` +# Database configuration +POSTGRES_DB=keycloak_db +POSTGRES_USER=keycloak_db_user +POSTGRES_PASSWORD=keycloak_db_user_password + +# Keycloak admin credentials +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=password + +# SSL configuration +SERVER_SSL_KEY_STORE_PASSWORD=changeit +SERVER_SSL_TRUST_STORE_PASSWORD=changeit +KC_HTTPS_KEY_STORE_PASSWORD=changeit +KC_HTTPS_TRUST_STORE_PASSWORD=changeit +KC_SPI_TRUSTSTORE_FILE_PASSWORD=changeit + +# Keycloak configuration +KC_HOSTNAME=localhost +KC_HOSTNAME_PORT=8080 +KC_HOSTNAME_STRICT_BACKCHANNEL=false +KC_HTTP_ENABLED=false +KC_HOSTNAME_STRICT_HTTPS=false +KC_HEALTH_ENABLED=true +KC_DB=postgres +KC_HTTPS_CLIENT_AUTH=required +KC_HTTPS_ENABLED=true +KC_HTTPS_PORT=8443 +KC_LOG_LEVEL=INFO +``` + +2. Start all services using Docker Compose: + +```bash +cd docker +docker-compose up -d +``` + +This will start: +- PostgreSQL database +- Keycloak authentication server +- Management Node application + +3. Access the application at https://localhost:8090 + +### Stopping the Services + +```bash +cd docker +docker-compose down +``` + +To remove volumes as well: + +```bash +docker-compose down -v +``` \ No newline at end of file diff --git a/docker/keycloak/docker-compose.yml b/docker/keycloak/docker-compose.yml new file mode 100644 index 0000000..d2a1453 --- /dev/null +++ b/docker/keycloak/docker-compose.yml @@ -0,0 +1,70 @@ + +services: + postgres: + image: postgres:16.2 + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + networks: + - keycloak_network + ports: + - "5433:5432" + keycloak: + image: quay.io/keycloak/keycloak:26.3.2 + command: start --verbose + hostname: localhost + container_name: keycloak + environment: + KC_HOSTNAME: localhost + KC_HOSTNAME_PORT: ${KC_HOSTNAME_PORT} + KC_HOSTNAME_STRICT_BACKCHANNEL: ${KC_HOSTNAME_STRICT_BACKCHANNEL} + KC_HTTP_ENABLED: ${KC_HTTP_ENABLED} + KC_HOSTNAME_STRICT_HTTPS: ${KC_HOSTNAME_STRICT_HTTPS} + KC_HEALTH_ENABLED: ${KC_HEALTH_ENABLED} + KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN} + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} + KC_DB: ${KC_DB} + KC_DB_URL: jdbc:postgresql://postgres/${POSTGRES_DB} + KC_DB_USERNAME: ${POSTGRES_USER} + KC_DB_PASSWORD: ${POSTGRES_PASSWORD} + ################################## + KC_HTTPS_CLIENT_AUTH: ${KC_HTTPS_CLIENT_AUTH} + KC_HTTPS_ENABLED: ${KC_HTTPS_ENABLED} + KC_HTTPS_PORT: ${KC_HTTPS_PORT} + KC_HTTPS_KEY_STORE_FILE: /cert/keystore.jks + KC_HTTPS_KEY_STORE_PASSWORD: ${KC_HTTPS_KEY_STORE_PASSWORD} + KC_HTTPS_CERTIFICATE_FILE: /cert/localhost.crt + KC_HTTPS_CERTIFICATE_KEY_FILE: /key/localhost.key + KC_HTTPS_TRUST_STORE_FILE: /keystores/localhost.p12 + KC_HTTPS_TRUST_STORE_PASSWORD: ${KC_HTTPS_TRUST_STORE_PASSWORD} + KC_SPI_TRUSTSTORE_FILE_FILE: /cert/truststore.jks + KC_SPI_TRUSTSTORE_FILE_PASSWORD: ${KC_SPI_TRUSTSTORE_FILE_PASSWORD} + ############################# + KC_LOG_LEVEL: ${KC_LOG_LEVEL} + + ports: + - "8080:8080" + - "8443:8443" + - "9000:9000" + restart: always + depends_on: + - postgres + networks: + - keycloak_network + volumes: + - ./localhost.p12:/keystores/localhost.p12 + - ./localhost.crt:/cert/localhost.crt + - ./localhost.key:/key/localhost.key + - ./keystore.jks:/cert/keystore.jks + - ./truststore.jks:/cert/truststore.jks + +volumes: + postgres_data: + driver: local + +networks: + keycloak_network: + driver: bridge \ No newline at end of file diff --git a/docker/keycloak/management-node-realm.json b/docker/keycloak/management-node-realm.json new file mode 100644 index 0000000..21b96bf --- /dev/null +++ b/docker/keycloak/management-node-realm.json @@ -0,0 +1,3348 @@ +{ + "id": "ef5e1bef-dc7b-4162-ae7a-2728f9c1429f", + "realm": "management-node", + "displayName": "", + "displayNameHtml": "", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 432000, + "accessTokenLifespanForImplicitFlow": 1296000, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 86400, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 864000, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxTemporaryLockouts": 0, + "bruteForceStrategy": "MULTIPLE", + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "3210c6df-ff5d-4b6e-9992-072aa063845b", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "ef5e1bef-dc7b-4162-ae7a-2728f9c1429f", + "attributes": {} + }, + { + "id": "fa6913b5-2e75-4815-8947-354605c79cd3", + "name": "default-roles-management-node", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ], + "client": { + "account": [ + "view-profile", + "manage-account" + ] + } + }, + "clientRole": false, + "containerId": "ef5e1bef-dc7b-4162-ae7a-2728f9c1429f", + "attributes": {} + }, + { + "id": "7e159690-fc88-4f6c-a940-5894a7528945", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "ef5e1bef-dc7b-4162-ae7a-2728f9c1429f", + "attributes": {} + }, + { + "id": "f9d6df79-21cb-4500-a610-773f669ae080", + "name": "Producer", + "description": "", + "composite": false, + "clientRole": false, + "containerId": "ef5e1bef-dc7b-4162-ae7a-2728f9c1429f", + "attributes": {} + }, + { + "id": "d50cd39e-766f-4bec-bde1-287eacbaf7af", + "name": "management-node-client-role", + "description": "management-node-client-role", + "composite": false, + "clientRole": false, + "containerId": "ef5e1bef-dc7b-4162-ae7a-2728f9c1429f", + "attributes": { + "management-node-client-role-attrib-1": [ + "attrib1 value" + ] + } + }, + { + "id": "a7c08fad-bb1e-42ba-bf6a-d0eea3556a2d", + "name": "Consumer", + "description": "", + "composite": false, + "clientRole": false, + "containerId": "ef5e1bef-dc7b-4162-ae7a-2728f9c1429f", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "189a79e6-7fe2-435a-ae30-04135840c920", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "attributes": {} + }, + { + "id": "23e00cc9-3581-4974-9798-1b2994ef8083", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "attributes": {} + }, + { + "id": "eecd5b90-2992-4d90-a9a3-fc5b6a276066", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "attributes": {} + }, + { + "id": "61213992-adc9-4667-9c21-07b62397bad0", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "attributes": {} + }, + { + "id": "999975bc-2e7f-4533-9078-5b9c4f5078c7", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "attributes": {} + }, + { + "id": "10b99448-13ad-44fd-a608-dced92a4c2bd", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "attributes": {} + }, + { + "id": "683db7b6-627d-4856-8203-86dcf8cf2984", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "attributes": {} + }, + { + "id": "6404e854-8e8c-431a-82f5-d017c4f765ae", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-groups", + "query-users" + ] + } + }, + "clientRole": true, + "containerId": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "attributes": {} + }, + { + "id": "66a51926-a39a-4e40-81a9-4d38fad90c29", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "attributes": {} + }, + { + "id": "bb3a98dd-9e4a-4dbf-a8e7-3a952d307fbd", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "attributes": {} + }, + { + "id": "7a5230a5-8d77-4d29-8178-e005de0fa854", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "attributes": {} + }, + { + "id": "bd6d39b0-436c-46cc-9cd5-7981e9fe811b", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "attributes": {} + }, + { + "id": "e4db3044-97f3-4476-9621-b9d0dba97193", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "attributes": {} + }, + { + "id": "c77706e2-0481-4134-9139-d53c3ac81065", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "attributes": {} + }, + { + "id": "08f38c5c-2c8c-4a53-be1c-1b8dc99bc64d", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "attributes": {} + }, + { + "id": "2c512d5d-8a8e-49a6-bfbb-51cce75c9c4e", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "attributes": {} + }, + { + "id": "ca3f6f20-3cf7-4da8-a9bd-75d4a51a86be", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "attributes": {} + }, + { + "id": "926fe7e3-cf74-4ddf-8b93-a4ff5d2c5217", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "attributes": {} + }, + { + "id": "7fbee1c4-f9f4-40b2-adb5-58e8fad7fb27", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "manage-clients", + "view-events", + "manage-events", + "manage-authorization", + "manage-realm", + "query-realms", + "view-identity-providers", + "view-users", + "view-clients", + "manage-identity-providers", + "manage-users", + "query-groups", + "query-users", + "create-client", + "view-realm", + "view-authorization", + "query-clients", + "impersonation" + ] + } + }, + "clientRole": true, + "containerId": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "attributes": {} + } + ], + "security-admin-console": [], + "FEDERATOR_BCC": [ + { + "id": "188c82b5-988d-45ab-8023-4257a153cdcd", + "name": "PendingPlanningApplications", + "description": "data product PendingPlanningApplications", + "composite": false, + "clientRole": true, + "containerId": "883ba4de-b575-4d0c-ac5f-e58608b181e0", + "attributes": {} + } + ], + "ztf-client": [], + "account-console": [], + "F1": [ + { + "id": "82cd2c43-9084-4c83-8b03-394c286cc8a7", + "name": "TOPIC_1", + "description": "", + "composite": false, + "clientRole": true, + "containerId": "a7a8f50a-0380-4b55-8731-f7a0b0cabf18", + "attributes": {} + }, + { + "id": "07cc8275-301a-4cf4-b76e-8982d18f6438", + "name": "TOPIC_2", + "description": "", + "composite": false, + "clientRole": true, + "containerId": "a7a8f50a-0380-4b55-8731-f7a0b0cabf18", + "attributes": {} + } + ], + "broker": [ + { + "id": "610f0c3d-ead4-4afd-a837-52d758757afa", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "ee91f049-e1b5-40ba-a0de-f3a6af2c71df", + "attributes": {} + } + ], + "F2": [ + { + "id": "b2d1659c-3026-48dc-a13e-187b214be2f7", + "name": "uma_protection", + "composite": false, + "clientRole": true, + "containerId": "104ef21e-8a92-4311-8aa6-2f91f7efb8a7", + "attributes": {} + }, + { + "id": "10d5cc44-0a67-4752-99ce-471a4cbaca92", + "name": "R1", + "description": "", + "composite": false, + "clientRole": true, + "containerId": "104ef21e-8a92-4311-8aa6-2f91f7efb8a7", + "attributes": {} + } + ], + "FEDERATOR_HEG": [ + { + "id": "4132df01-9c4a-4d04-8a39-45218efcc183", + "name": "BrownfieldLandAvailability", + "description": "BrownfieldLandAvailability", + "composite": false, + "clientRole": true, + "containerId": "68efa081-04be-4592-8fd7-ec8226df4406", + "attributes": {} + } + ], + "FEDERATOR_ENV": [ + { + "id": "cc91a4c8-07c7-4e83-8cf7-a776775e8f32", + "name": "FloodRiskMapZones", + "description": "FloodRiskMapZones", + "composite": false, + "clientRole": true, + "containerId": "7b29c54b-f77a-4eea-80db-d480b2cb801a", + "attributes": {} + } + ], + "admin-cli": [], + "CLIENTX": [ + { + "id": "c5be2d07-0f36-44d0-91ff-4512787dc13a", + "name": "uma_protection", + "composite": false, + "clientRole": true, + "containerId": "82731231-ea52-4aac-97a5-d7f4be55e532", + "attributes": {} + }, + { + "id": "b8713d26-7132-4dea-a0d4-c72946094363", + "name": "TOPIX_1", + "description": "", + "composite": false, + "clientRole": true, + "containerId": "82731231-ea52-4aac-97a5-d7f4be55e532", + "attributes": {} + } + ], + "management-node": [ + { + "id": "2ecc5fd0-7774-4d2d-8afc-690c9660dece", + "name": "access_consumer_configurations", + "description": "", + "composite": false, + "clientRole": true, + "containerId": "e85837ed-4172-4d95-b21b-f71d7a23cf31", + "attributes": {} + }, + { + "id": "dda9430c-36a1-49fb-ba1d-3679a511c696", + "name": "access_producer_configurations", + "description": "", + "composite": false, + "clientRole": true, + "containerId": "e85837ed-4172-4d95-b21b-f71d7a23cf31", + "attributes": {} + } + ], + "account": [ + { + "id": "8d2ea143-a576-42a4-9b4e-fdb13899fafa", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "64f6f28a-a268-412b-b787-a671ddbfb17e", + "attributes": {} + }, + { + "id": "9d4a51f4-8e9e-45c5-bf40-a1f74755c08a", + "name": "view-groups", + "description": "${role_view-groups}", + "composite": false, + "clientRole": true, + "containerId": "64f6f28a-a268-412b-b787-a671ddbfb17e", + "attributes": {} + }, + { + "id": "65670d5a-b9c7-4e00-82b0-050e587dc8f7", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "64f6f28a-a268-412b-b787-a671ddbfb17e", + "attributes": {} + }, + { + "id": "7c4a6bec-9a2f-484f-81e9-3eb84a784ad7", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "64f6f28a-a268-412b-b787-a671ddbfb17e", + "attributes": {} + }, + { + "id": "0c35a16f-acd4-41b5-9074-48a45b5caa04", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "64f6f28a-a268-412b-b787-a671ddbfb17e", + "attributes": {} + }, + { + "id": "4edcb0a4-c6f5-4751-995e-3204d8562022", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "64f6f28a-a268-412b-b787-a671ddbfb17e", + "attributes": {} + }, + { + "id": "16854882-d56d-4182-b5fd-a8e1ccb25557", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "64f6f28a-a268-412b-b787-a671ddbfb17e", + "attributes": {} + }, + { + "id": "43d27628-4c6b-446c-8f29-3af86d3504e2", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "64f6f28a-a268-412b-b787-a671ddbfb17e", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRole": { + "id": "fa6913b5-2e75-4815-8947-354605c79cd3", + "name": "default-roles-management-node", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "ef5e1bef-dc7b-4162-ae7a-2728f9c1429f" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppFreeOTPName", + "totpAppGoogleName", + "totpAppMicrosoftAuthenticatorName" + ], + "localizationTexts": {}, + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyExtraOrigins": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "webAuthnPolicyPasswordlessExtraOrigins": [], + "users": [ + { + "id": "4588d203-c6ea-4748-a3bf-c98836e2676e", + "username": "service-account-clientx", + "emailVerified": false, + "enabled": true, + "createdTimestamp": 1753490970588, + "totp": false, + "serviceAccountClientId": "CLIENTX", + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "default-roles-management-node" + ], + "clientRoles": { + "CLIENTX": [ + "uma_protection" + ] + }, + "notBefore": 0, + "groups": [] + }, + { + "id": "6847276f-7f7c-4b58-9e02-0cf1b092a36f", + "username": "service-account-f2", + "emailVerified": false, + "enabled": true, + "createdTimestamp": 1753489610213, + "totp": false, + "serviceAccountClientId": "F2", + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "default-roles-management-node" + ], + "clientRoles": { + "F2": [ + "uma_protection", + "R1" + ] + }, + "notBefore": 0, + "groups": [] + }, + { + "id": "9dfa9d59-4e72-4ce6-ae79-d39978044285", + "username": "service-account-federator_bcc", + "emailVerified": false, + "enabled": true, + "createdTimestamp": 1753915937336, + "totp": false, + "serviceAccountClientId": "FEDERATOR_BCC", + "disableableCredentialTypes": [], + "requiredActions": [], + "clientRoles": { + "FEDERATOR_HEG": [ + "BrownfieldLandAvailability" + ], + "management-node": [ + "access_consumer_configurations", + "access_producer_configurations" + ] + }, + "notBefore": 0, + "groups": [] + }, + { + "id": "a4ee07e0-d7ad-42dc-bde6-62beb7a4f07b", + "username": "service-account-federator_env", + "emailVerified": false, + "enabled": true, + "createdTimestamp": 1753916007876, + "totp": false, + "serviceAccountClientId": "FEDERATOR_ENV", + "disableableCredentialTypes": [], + "requiredActions": [], + "notBefore": 0, + "groups": [] + }, + { + "id": "7acb1f08-96fb-4b73-8dc8-085a40c6f03b", + "username": "service-account-federator_heg", + "emailVerified": false, + "enabled": true, + "createdTimestamp": 1753915775442, + "totp": false, + "serviceAccountClientId": "FEDERATOR_HEG", + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "default-roles-management-node" + ], + "notBefore": 0, + "groups": [] + }, + { + "id": "86a41a8a-ab2e-465e-8b48-a09d3275f842", + "username": "service-account-management-node", + "emailVerified": false, + "enabled": true, + "createdTimestamp": 1753486541848, + "totp": false, + "serviceAccountClientId": "management-node", + "disableableCredentialTypes": [], + "requiredActions": [], + "notBefore": 0, + "groups": [] + }, + { + "id": "183affa4-4419-4830-9c76-2ab7f5d687f9", + "username": "service-account-ztf-client", + "emailVerified": false, + "enabled": true, + "createdTimestamp": 1753647011210, + "totp": false, + "serviceAccountClientId": "ztf-client", + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "default-roles-management-node" + ], + "notBefore": 0, + "groups": [] + } + ], + "scopeMappings": [ + { + "clientScope": "test_client_scope", + "roles": [ + "offline_access" + ] + }, + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "CLIENTX": [ + { + "clientScope": "test_client_scope", + "roles": [ + "TOPIX_1" + ] + } + ], + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account", + "view-groups" + ] + } + ] + }, + "clients": [ + { + "id": "64f6f28a-a268-412b-b787-a671ddbfb17e", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/management-node/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/management-node/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "670e8357-a956-4bdc-a7ab-7c576fc3dfc9", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/management-node/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/management-node/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "7c2c99c6-639d-48bb-9bb2-d6ddb51a19d1", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "acf9c062-bdc3-41cd-abfb-15f399a31418", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "client.use.lightweight.access.token.enabled": "true" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "ee91f049-e1b5-40ba-a0de-f3a6af2c71df", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "true" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "82731231-ea52-4aac-97a5-d7f4be55e532", + "clientId": "CLIENTX", + "name": "CLIENTX", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1753490970", + "backchannel.logout.session.required": "true", + "standard.token.exchange.enabled": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "service_account", + "web-origins", + "roles", + "management-node-client-scope" + ], + "optionalClientScopes": [ + "Sample_ORG" + ], + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Default Resource", + "type": "urn:CLIENTX:resources:default", + "ownerManagedAccess": false, + "attributes": {}, + "uris": [ + "/*" + ] + } + ], + "policies": [ + { + "name": "Default Policy", + "description": "A policy that grants access only for users within this realm", + "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" + } + }, + { + "name": "Default Permission", + "description": "A permission that applies to the default resource type", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "defaultResourceType": "urn:CLIENTX:resources:default", + "applyPolicies": "[\"Default Policy\"]" + } + } + ], + "scopes": [], + "decisionStrategy": "UNANIMOUS" + } + }, + { + "id": "a7a8f50a-0380-4b55-8731-f7a0b0cabf18", + "clientId": "F1", + "name": "", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1753486325", + "backchannel.logout.session.required": "true", + "standard.token.exchange.enabled": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "roles", + "management-node-client-scope" + ], + "optionalClientScopes": [] + }, + { + "id": "104ef21e-8a92-4311-8aa6-2f91f7efb8a7", + "clientId": "F2", + "name": "F2", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1753489610", + "backchannel.logout.session.required": "true", + "standard.token.exchange.enabled": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "service_account", + "roles", + "management-node-client-scope" + ], + "optionalClientScopes": [], + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Default Resource", + "type": "urn:F2:resources:default", + "ownerManagedAccess": false, + "attributes": {}, + "uris": [ + "/*" + ] + } + ], + "policies": [ + { + "name": "Default Policy", + "description": "A policy that grants access only for users within this realm", + "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" + } + }, + { + "name": "Default Permission", + "description": "A permission that applies to the default resource type", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "defaultResourceType": "urn:F2:resources:default", + "applyPolicies": "[\"Default Policy\"]" + } + } + ], + "scopes": [], + "decisionStrategy": "UNANIMOUS" + } + }, + { + "id": "883ba4de-b575-4d0c-ac5f-e58608b181e0", + "clientId": "FEDERATOR_BCC", + "name": "Bristol City Council (BCC)", + "description": "Bristol City Council (BCC)", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-x509", + "secret": "**********", + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1753915937", + "x509.subjectdn": "(.*?)(?:$)", + "backchannel.logout.session.required": "false", + "standard.token.exchange.enabled": "false", + "frontchannel.logout.session.required": "true", + "oauth2.device.authorization.grant.enabled": "false", + "display.on.consent.screen": "false", + "x509.allow.regex.pattern.comparison": "true", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "FEDERATOR_PRODUCER", + "service_account", + "roles", + "FEDERATOR_CONSUMER" + ], + "optionalClientScopes": [ + "web-origins", + "Sample_ORG", + "test_client_scope", + "management-node-client-scope" + ] + }, + { + "id": "7b29c54b-f77a-4eea-80db-d480b2cb801a", + "clientId": "FEDERATOR_ENV", + "name": "Environment Agency (ENV)", + "description": "Environment Agency (ENV)", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-x509", + "secret": "**********", + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1753916007", + "x509.subjectdn": "(.*?)(?:$)", + "backchannel.logout.session.required": "true", + "standard.token.exchange.enabled": "false", + "frontchannel.logout.session.required": "true", + "oauth2.device.authorization.grant.enabled": "false", + "display.on.consent.screen": "false", + "x509.allow.regex.pattern.comparison": "true", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "service_account", + "web-origins", + "Sample_ORG", + "roles", + "management-node-client-scope" + ], + "optionalClientScopes": [ + "test_client_scope" + ], + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Default Resource", + "type": "urn:FEDERATOR_ENV:resources:default", + "ownerManagedAccess": false, + "attributes": {}, + "uris": [ + "/*" + ] + } + ], + "policies": [ + { + "name": "Default Policy", + "description": "A policy that grants access only for users within this realm", + "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" + } + }, + { + "name": "Default Permission", + "description": "A permission that applies to the default resource type", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "defaultResourceType": "urn:FEDERATOR_ENV:resources:default", + "applyPolicies": "[\"Default Policy\"]" + } + } + ], + "scopes": [], + "decisionStrategy": "UNANIMOUS" + } + }, + { + "id": "68efa081-04be-4592-8fd7-ec8226df4406", + "clientId": "FEDERATOR_HEG", + "name": "Home England Federator", + "description": "Home England Federator", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-x509", + "secret": "**********", + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1753915775", + "x509.subjectdn": "(.*?)(?:$)", + "backchannel.logout.session.required": "true", + "standard.token.exchange.enabled": "false", + "frontchannel.logout.session.required": "true", + "oauth2.device.authorization.grant.enabled": "false", + "display.on.consent.screen": "false", + "use.jwks.url": "false", + "x509.allow.regex.pattern.comparison": "true", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "service_account", + "web-origins", + "Sample_ORG", + "roles", + "management-node-client-scope" + ], + "optionalClientScopes": [ + "test_client_scope" + ], + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Default Resource", + "type": "urn:FEDERATOR_HEG:resources:default", + "ownerManagedAccess": false, + "attributes": {}, + "uris": [ + "/*" + ] + } + ], + "policies": [ + { + "name": "Default Policy", + "description": "A policy that grants access only for users within this realm", + "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" + } + }, + { + "name": "Default Permission", + "description": "A permission that applies to the default resource type", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "defaultResourceType": "urn:FEDERATOR_HEG:resources:default", + "applyPolicies": "[\"Default Policy\"]" + } + } + ], + "scopes": [], + "decisionStrategy": "UNANIMOUS" + } + }, + { + "id": "e85837ed-4172-4d95-b21b-f71d7a23cf31", + "clientId": "management-node", + "name": "management-node", + "description": "management-node-id", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "request.object.signature.alg": "any", + "frontchannel.logout.session.required": "true", + "oauth2.device.authorization.grant.enabled": "false", + "use.jwks.url": "true", + "backchannel.logout.revoke.offline.tokens": "false", + "use.refresh.tokens": "false", + "jwt.credential.certificate": "MIICszCCAZsCBgGYQkHM5DANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDDBJtYW5hZ2VtZW50LW5vZGUtaWQwHhcNMjUwNzI1MTU0MjQ1WhcNMzUwNzI1MTU0NDI1WjAdMRswGQYDVQQDDBJtYW5hZ2VtZW50LW5vZGUtaWQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCe/kRQjFNsLnXlgPjAHb5fyw1yYz3lMiWT89yqe4iTVAUA+pSG239clCWcdM8cer6lfGmjRJQD/ZspOI+2DMT5hdyx4neMa2YTBg+VeXvPN5mngs9j3t73fh/KqDdoW9HeJKovUahRJxnJJm8y9XFBnNMyDMOW7KyvajxqcZFJtuN1TQaqcN1eOW1Vge7GFqYu/M2+T1XZzqS7nFDTYu6cIrxuZHMLcTuvVJNoQAJb36wtztUjO031kw/WjhWvLzc3Wcy94HWeWYcUcMtmWkOlMWX6pa79bBmRECet4w4KTamm2UA0IpfAexhcyIT5cqFJCinkWNySD3DuzB0Kz/g7AgMBAAEwDQYJKoZIhvcNAQELBQADggEBACvnEy+ND2Jn5qxT93XLPm8Kn6JwmqtHQNJkAaHYVVK5NJ2Gi8QXTku0fOjydxfmhWynM/YXpf4Y0Hx7lvwDhhJA+wOstVhOkuKRrM8CK4ZDorREMaJPOiNRqepqWm3bekiamrFH4KmHwn18ufChURYCamw/7LQyY3LtvbXrnNs4RyxtaGq6UKzoTAQ3L4vOjqsTeQQm8TdtPhlNxJ7nVW/S8VFgTL6LSHRCr2k2A4HYLQCS9r1tmSJTdErn2hioOSbcYMPq1i+3PhTDVEGYG1sd4+CNqckfw2hHDpvuZ9glaNGM8NulY240CO4i0GZXYhHsDe8DEaH/0zDKamTAssk=", + "realm_client": "false", + "oidc.ciba.grant.enabled": "false", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "require.pushed.authorization.requests": "false", + "request.object.encryption.enc": "any", + "client.secret.creation.time": "1753458211", + "request.object.encryption.alg": "any", + "client.introspection.response.allow.jwt.claim.enabled": "false", + "standard.token.exchange.enabled": "false", + "client.use.lightweight.access.token.enabled": "false", + "request.object.required": "not required", + "access.token.header.type.rfc9068": "false", + "tls.client.certificate.bound.access.tokens": "false", + "acr.loa.map": "{}", + "display.on.consent.screen": "false", + "x509.allow.regex.pattern.comparison": "false", + "token.response.type.bearer.lower-case": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "7b3d73de-07ce-45e6-8bec-424d26a4de70", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "id": "7528d924-630c-4bb4-8344-dd5e8b64450c", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + }, + { + "id": "bf1b69e7-b52e-4109-a5c4-25ca9bf2abc7", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "client_id", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "client_id", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "service_account", + "Sample_ORG", + "roles", + "test_client_scope", + "management-node-client-scope" + ], + "optionalClientScopes": [ + "acr", + "address", + "phone", + "offline_access", + "profile", + "microprofile-jwt", + "basic", + "email" + ], + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Default Resource", + "type": "urn:management-node:resources:default", + "ownerManagedAccess": false, + "attributes": {}, + "uris": [ + "/*" + ] + } + ], + "policies": [ + { + "name": "Default Policy", + "description": "A policy that grants access only for users within this realm", + "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" + } + }, + { + "name": "Default Permission", + "description": "A permission that applies to the default resource type", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "defaultResourceType": "urn:management-node:resources:default", + "applyPolicies": "[\"Default Policy\"]" + } + } + ], + "scopes": [], + "decisionStrategy": "UNANIMOUS" + } + }, + { + "id": "b27e4e1e-a764-4f72-a262-d0040046aa98", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "true" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "ce5a2151-e241-4d0b-9b96-a16a36e797fc", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/management-node/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/management-node/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "client.use.lightweight.access.token.enabled": "true", + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "e0085656-07c2-43b2-a737-002ce6c7b25d", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "794987a5-c74c-4a08-822a-87ffb420aec7", + "clientId": "ztf-client", + "name": "Zero Trust Company", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-x509", + "secret": "**********", + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": true, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1753647011", + "x509.subjectdn": "(.*?)(?:$)", + "backchannel.logout.session.required": "true", + "standard.token.exchange.enabled": "false", + "frontchannel.logout.session.required": "true", + "oauth2.device.authorization.grant.enabled": "false", + "display.on.consent.screen": "false", + "use.jwks.url": "false", + "x509.allow.regex.pattern.comparison": "true", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "service_account", + "web-origins", + "Sample_ORG", + "roles", + "management-node-client-scope" + ], + "optionalClientScopes": [ + "test_client_scope" + ] + } + ], + "clientScopes": [ + { + "id": "f4426743-f970-43fd-9344-19881b4693e7", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "e97861f2-e5db-453d-a171-c0c8192a82d0", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "ec6bd9f5-95e5-4bc7-8d0c-1c2ab9f3bedc", + "name": "basic", + "description": "OpenID Connect scope for add all basic claims to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "801ea979-d1e4-477e-b997-4fb834680080", + "name": "sub", + "protocol": "openid-connect", + "protocolMapper": "oidc-sub-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "id": "793f4660-81f5-4434-b403-7872282bf410", + "name": "auth_time", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "AUTH_TIME", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "auth_time", + "jsonType.label": "long" + } + } + ] + }, + { + "id": "93f95734-ff72-4851-af7c-6e7d2df3bf94", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "consent.screen.text": "", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "61bccb09-87c2-45be-8134-d4b7c00ac761", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "f2f3894f-a501-4d7b-aa86-69bdeeea2ea9", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${phoneScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "9abc9193-3e31-47a8-9b3b-8791f0247282", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "6db219a3-60e8-47c6-8a0a-252044dd3b68", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "fc360ebc-5ba7-47bd-a46c-4d87ad446a14", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${addressScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "971c9cc8-6627-43dc-b9d2-cba4c4cf279d", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "introspection.token.claim": "true", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "69a797a7-1168-4da8-9859-2c57092cc996", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${emailScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "08229d6e-aaec-4b55-82be-ebc9bbc5dfd4", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "da0c2a81-0d71-4ffa-9956-f7df57753fcd", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "f6a53be9-cc84-4a85-a401-7f135144d56b", + "name": "Sample_ORG", + "description": "", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "gui.order": "", + "consent.screen.text": "" + } + }, + { + "id": "7407140b-03bd-4f36-9832-74fe0cad00ce", + "name": "FEDERATOR_PRODUCER", + "description": "", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "gui.order": "", + "consent.screen.text": "" + } + }, + { + "id": "56fe4473-b7a0-4c09-8b04-c6da3e999e9c", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "c8335cc9-4efe-4cdb-b7be-750987240561", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "7e991b0d-1a53-4130-a3bc-92592bf42c23", + "name": "FEDERATOR_CONSUMER", + "description": "", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false", + "gui.order": "", + "consent.screen.text": "" + } + }, + { + "id": "281363a6-bc8d-44d5-9320-a9a72227d354", + "name": "management-node-client-scope", + "description": "", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false", + "gui.order": "", + "consent.screen.text": "" + } + }, + { + "id": "935e1cf0-6631-4705-b987-e20363e34216", + "name": "service_account", + "description": "Specific scope for a client enabled for service accounts", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "52ad466a-b858-4b05-9c7a-1b149ba6152b", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "id": "0280c783-a679-4321-86eb-d9d2487ab2d6", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "client_id", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "client_id", + "jsonType.label": "String" + } + }, + { + "id": "3cafc44c-fc63-4ced-be49-73ad0a1ea5c3", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "2dfac2b5-71b3-43d1-8722-668d4d8591d2", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "765c2ffa-4143-4568-b29a-d49021342ddc", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + }, + { + "id": "4ea1de00-8a65-410e-bf15-e4c6fb5fcc2f", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "f2db883f-5596-428e-bd08-195acce3b7ed", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${profileScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "50e049b8-67e7-470e-9d1f-99ad3c7a0d85", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "68e7ffa7-450e-43f6-9a66-ec60374494fa", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "031f217c-fa5c-4625-8e8f-4f5685698551", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "bf7debb6-9537-4124-a6fc-e96da6b591bf", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "9e8904b7-b3cb-4b9e-9538-b08181a716fe", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "058de1c6-2ff4-464a-9a6a-5836b5f8f1c1", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "59e3524d-8b08-42fa-9b0a-1b0b62d8d43e", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "2b8fc9f7-1da0-447f-b7b0-0d93fc794f8f", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "87b8c44d-cf89-42b7-b607-4d4ec912d7e2", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "4374c29e-a6bb-4479-a3b1-23e8604301c6", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "51279859-f298-4718-9799-95bf99dc89de", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "b4d0a893-a620-484b-89aa-c983c4073163", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + }, + { + "id": "17c09cff-2981-4c05-965a-aad8473a3bb8", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "f79f04bf-b338-4dc3-9e53-62ad191871c2", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "d3757b6b-3496-4601-b124-c3282a946dca", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "consent.screen.text": "${rolesScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "e164eb19-da7c-4c0e-9e97-f490532fc33c", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "44f71df4-bdea-4d50-84dc-ceb987c1efb4", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "userinfo.token.claim": "false", + "user.attribute": "foo", + "id.token.claim": "false", + "lightweight.claim": "false", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String" + } + }, + { + "id": "47c65d55-3b77-4eee-845f-3fe149d11dc5", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "176da5d5-648f-49a9-803b-7d7ecc4d831f", + "name": "test_client_scope", + "description": "", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "gui.order": "", + "consent.screen.text": "" + } + }, + { + "id": "37cf32a7-ad44-4695-9fc8-f1a71b965f58", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + } + ], + "defaultDefaultClientScopes": [ + "roles", + "web-origins", + "service_account", + "Sample_ORG" + ], + "defaultOptionalClientScopes": [ + "test_client_scope", + "FEDERATOR_PRODUCER", + "FEDERATOR_CONSUMER", + "management-node-client-scope" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "referrerPolicy": "no-referrer", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "1c2d0794-1129-4ab8-b054-93f455f01828", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "293591a2-0be4-49fa-aadf-8a77aa886bd9", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "e8a66e06-00f9-4cda-9648-5e47a9ec2e03", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "12dfdde7-c365-4cbb-bd14-cbd742d50a70", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-user-attribute-mapper", + "oidc-full-name-mapper", + "oidc-usermodel-attribute-mapper", + "saml-user-property-mapper", + "oidc-address-mapper", + "saml-role-list-mapper" + ] + } + }, + { + "id": "c4df1e03-967e-4aa4-9893-d2d9dda23e3a", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-full-name-mapper", + "saml-role-list-mapper", + "oidc-address-mapper", + "oidc-usermodel-property-mapper", + "saml-user-property-mapper", + "oidc-sha256-pairwise-sub-mapper" + ] + } + }, + { + "id": "3621e717-e423-4dc6-bd0c-b2e0e3122b94", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "cd8fb37f-167c-4ca9-98a7-5f790f35c7d6", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "f7c31898-9f63-4935-8056-0950d11af105", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + } + ], + "org.keycloak.userprofile.UserProfileProvider": [ + { + "id": "3d1ceab5-1289-4e5c-a0bb-a6a19ba8dd95", + "providerId": "declarative-user-profile", + "subComponents": {}, + "config": { + "kc.user.profile.config": [ + "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}],\"unmanagedAttributePolicy\":\"ENABLED\"}" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "d952caf4-4d03-4af6-954b-9adaea905b94", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } + }, + { + "id": "97b537bf-93d7-4c7f-b05a-87dd15f524ad", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "c7ba9315-ab2b-4896-b1e3-1de71aadbcbd", + "name": "hmac-generated-hs512", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS512" + ] + } + }, + { + "id": "9d67e760-7363-4f7e-a35e-418f4141a8eb", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "8e9b9977-59ee-4415-b236-66c91160d64e", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "authenticationFlows": [ + { + "id": "bed3bd3e-1b60-4021-a3c7-1cc515fbde49", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "7998d06d-e46f-426c-a39b-2e0a8fe84892", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "ce099c81-c280-432f-a6b1-e81c89adfeb2", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "2e1a942a-598f-4179-a4ad-f96c2351eb92", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "c864137e-3245-4efb-bba3-b235a7bfaf0b", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "9acb5bcd-71af-4ee0-b5fd-845bfaf84df1", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "36e05ae9-c63f-4f63-957e-463d3d93c390", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "75ce5060-b836-480e-a074-d4c977bb7c87", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "2ff9d4dc-3a9c-49e8-a63b-a0fdf4ddf92d", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "182c89a9-63f1-45c8-8474-38b2305e155f", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "1fbc0af6-bba0-4c3a-a9c1-a578ead77ee5", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "3d5f2c59-cc19-444a-87f8-f394b8856461", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "e17cd2c8-769a-4ebc-a9a3-3edd9c8a7819", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "b287700c-5606-4013-a3f7-87b0b8eaa724", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "7ae2dc53-966c-45ab-af3d-8717f1b0c2d1", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "a12b4ff6-d6ec-4973-a9c9-b2c23f6086cb", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "0671ae25-288d-4646-ac42-38e5ca08af48", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "52c7ce33-2d6d-447f-a579-304b459ba1bc", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "26c3bec5-dc3e-4c3e-8d3d-d03135a7ff71", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "4939fecb-f1e0-4c8d-8a15-65b418a086dd", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "delete_credential", + "name": "Delete Credential", + "providerId": "delete_credential", + "enabled": true, + "defaultAction": false, + "priority": 100, + "config": {} + }, + { + "alias": "idp_link", + "name": "Linking Identity Provider", + "providerId": "idp_link", + "enabled": true, + "defaultAction": false, + "priority": 110, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "firstBrokerLoginFlow": "first broker login", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DevicePollingInterval": "5", + "clientOfflineSessionMaxLifespan": "0", + "clientSessionIdleTimeout": "0", + "actionTokenGeneratedByUserLifespan.verify-email": "", + "actionTokenGeneratedByUserLifespan.idp-verify-account-via-email": "", + "clientOfflineSessionIdleTimeout": "0", + "actionTokenGeneratedByUserLifespan.execute-actions": "", + "cibaInterval": "5", + "realmReusableOtpCode": "false", + "cibaExpiresIn": "120", + "oauth2DeviceCodeLifespan": "864000", + "saml.signature.algorithm": "", + "parRequestUriLifespan": "60", + "clientSessionMaxLifespan": "0", + "frontendUrl": "", + "acr.loa.map": "{}", + "shortVerificationUri": "", + "actionTokenGeneratedByUserLifespan.reset-credentials": "" + }, + "keycloakVersion": "26.3.2", + "userManagedAccessAllowed": false, + "organizationsEnabled": true, + "verifiableCredentialsEnabled": false, + "adminPermissionsEnabled": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} \ No newline at end of file diff --git a/docs/JACOCO_COVERAGE.md b/docs/JACOCO_COVERAGE.md new file mode 100644 index 0000000..345c33a --- /dev/null +++ b/docs/JACOCO_COVERAGE.md @@ -0,0 +1,88 @@ +# JaCoCo Code Coverage Setup + +## Overview + +This document describes the JaCoCo code coverage setup for the Management Node application. JaCoCo has been configured to measure code coverage and ensure that it meets the specified thresholds. + +## Current Configuration + +JaCoCo has been configured in the `pom.xml` file with the following settings: + +1. **Coverage Thresholds**: Currently set to 50% for: + - Instructions + - Branches + - Lines + - Methods + - Classes + +2. **Excluded Packages/Classes**: + - DTOs (`**/dto/**`) + - Entity classes (`**/entity/**`) + - Configuration classes (`**/config/**`) + - Exception classes (`**/exception/**`) + - Main application class (`**/ManagementNodeApplication.java`) + +3. **Build Configuration**: + - Tests will run even if they fail (`testFailureIgnore=true` in maven-surefire-plugin) + - Coverage checks will not fail the build if thresholds aren't met (`haltOnFailure=false`) + +## Current Coverage Levels + +As of the latest build, the coverage levels are: +- Branches: 0% +- Lines: 22% +- Methods: 33% + +These are below the current thresholds of 50%, and significantly below the target of 80%. + +## Running the Coverage Report + +To generate the JaCoCo coverage report, run: + +```bash +./mvnw clean verify +``` + +The report will be generated in the `target/site/jacoco` directory. Open `target/site/jacoco/index.html` in a web browser to view the detailed coverage report. + +## Recommendations for Improving Coverage + +To reach the target of 80% code coverage: + +1. **Fix Failing Tests**: + - Address the NullPointerException in `ConsumerAllowedDataProviderServiceImplTest` + - Ensure all existing tests pass + +2. **Add More Tests**: + - Focus on adding tests for uncovered branches + - Increase method coverage by testing all public methods + - Prioritize testing business logic and service implementations + +3. **Gradual Threshold Increase**: + - Once coverage improves, gradually increase thresholds in the JaCoCo configuration + - Aim for incremental improvements: 50% → 60% → 70% → 80% + +4. **Consider Additional Exclusions**: + - If certain classes are not practical to test, consider adding them to the exclusions + - Document the rationale for any exclusions + +## Final Goal + +The final goal is to achieve 80% code coverage across all metrics: +- 80% instruction coverage +- 80% branch coverage +- 80% line coverage +- 80% method coverage +- 80% class coverage + +Once this goal is achieved, update the JaCoCo configuration to: +1. Set all thresholds to 80% +2. Set `haltOnFailure` to `true` to enforce the coverage requirements + +## Best Practices + +1. **Write Tests First**: Follow Test-Driven Development (TDD) principles +2. **Focus on Quality**: Aim for meaningful tests that verify behavior, not just increase coverage +3. **Regular Monitoring**: Check coverage reports regularly to identify areas needing improvement +4. **Integration with CI/CD**: Include coverage checks in your CI/CD pipeline +5. **Documentation**: Keep this document updated with changes to coverage configuration \ No newline at end of file diff --git a/docs/MOCKITO_USAGE.md b/docs/MOCKITO_USAGE.md new file mode 100644 index 0000000..888864d --- /dev/null +++ b/docs/MOCKITO_USAGE.md @@ -0,0 +1,123 @@ +# Mockito Testing Tool Usage Guide + +## Overview + +Mockito is a popular mocking framework for Java that allows you to create and configure mock objects. Using Mockito, you can verify that certain methods are called with certain parameters, stub method calls to return specific values, and more. + +This guide explains how Mockito has been integrated into the project and provides examples of how to use it for testing. + +## Dependencies Added + +The following dependencies have been added to the project's `pom.xml`: + +```xml + + org.mockito + mockito-core + 5.10.0 + test + + + org.mockito + mockito-junit-jupiter + 5.10.0 + test + +``` + +## Basic Mockito Usage + +### Setting Up Mockito in a Test Class + +To use Mockito with JUnit 5, add the `@ExtendWith(MockitoExtension.class)` annotation to your test class: + +```java +@ExtendWith(MockitoExtension.class) +class MyServiceTest { + // Test methods +} +``` + +### Creating Mock Objects + +Use the `@Mock` annotation to create mock objects: + +```java +@Mock +private DependencyService dependencyService; +``` + +### Injecting Mocks + +Use the `@InjectMocks` annotation to inject mock objects into the class under test: + +```java +@InjectMocks +private MyService myService; +``` + +## Mockito Examples + +### Stubbing Method Calls + +```java +// Stub a method to return a specific value +when(dependencyService.getData()).thenReturn(expectedData); + +// Stub a method with any argument of a specific type +when(dependencyService.processData(any(Data.class))).thenReturn(processedData); + +// Stub a method with a specific argument +when(dependencyService.findById("1")).thenReturn(Optional.of(testData)); + +// Stub a method with a combination of specific and any arguments +when(dependencyService.updateData(eq("1"), any(Data.class))).thenReturn(Optional.of(updatedData)); +``` + +### Verifying Method Calls + +```java +// Verify that a method was called exactly once +verify(dependencyService, times(1)).getData(); + +// Verify that a method was called with a specific argument +verify(dependencyService, times(1)).findById("1"); + +// Verify that a method was called with a combination of specific and any arguments +verify(dependencyService, times(1)).updateData(eq("1"), any(Data.class)); +``` + +## Advanced Mockito Features + +Mockito offers many advanced features not covered in the examples: + +1. **Argument Captors**: Capture arguments passed to methods for further verification +2. **Spies**: Create partial mocks that call real methods but can still be verified and stubbed +3. **Verification Modes**: Verify method calls with different modes like `atLeastOnce()`, `atMost(n)`, etc. +4. **Answer Interfaces**: Provide custom answers for stubbed methods +5. **Verification Timeouts**: Verify method calls with timeouts for concurrent code + +For more information, refer to the [Mockito documentation](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html). + +## Best Practices + +1. **Keep Tests Focused**: Each test should verify a single behavior +2. **Use Descriptive Test Names**: Test names should describe what they're testing +3. **Minimize Stubbing**: Only stub methods that are necessary for the test +4. **Verify Important Interactions**: Only verify method calls that are important for the test +5. **Use Argument Matchers Consistently**: If you use an argument matcher for one argument, you must use matchers for all arguments in that method call +6. **Reset Mocks When Necessary**: Use `reset(mock)` when you need to reset a mock's state between tests + +## Troubleshooting + +### Common Issues + +1. **"Invalid use of argument matchers"**: If you use an argument matcher for one argument, you must use matchers for all arguments in that method call +2. **"Wanted but not invoked"**: The method you're verifying was not called with the specified arguments +3. **"Unnecessary stubbing"**: You stubbed a method that was not called during the test + +### Solutions + +1. Use `any()`, `eq()`, or other matchers consistently for all arguments +2. Check that the method is being called with the expected arguments +3. Remove unnecessary stubbing or add `lenient()` to the stubbing \ No newline at end of file diff --git a/docs/MTLS_CONFIGURATION.md b/docs/MTLS_CONFIGURATION.md new file mode 100644 index 0000000..7cdcdb5 --- /dev/null +++ b/docs/MTLS_CONFIGURATION.md @@ -0,0 +1,160 @@ +# MTLS Configuration Guide + +This guide provides detailed instructions on how to configure Mutual TLS (MTLS) for both the Keycloak authentication server and the Management Node Spring Boot application. + +## What is MTLS and Why It's Needed + +Mutual TLS (MTLS) is a security protocol that requires both the client and server to authenticate each other using X.509 certificates. Unlike standard TLS where only the server authenticates itself to the client, MTLS ensures bidirectional authentication, providing a higher level of security. + +In the context of the Management Node Module: +- MTLS establishes a zero-trust security model where all communications must be authenticated and encrypted +- It provides service-to-service authentication without relying on passwords or API keys +- It prevents unauthorized access to sensitive APIs and data + +## Certificate Files Overview + +Before configuring MTLS, ensure you have the following certificate files: + +| File Type | Purpose | Used By | +|-----------|---------|---------| +| `keystore.jks` | Java keystore containing the server certificate and private key | Keycloak & Management Node | +| `truststore.jks` | Java truststore containing trusted client certificates | Keycloak & Management Node | +| `localhost.p12` | PKCS12 keystore for client authentication | Keycloak | +| `localhost.crt` | Certificate file | Keycloak | +| `localhost.key` | Private key file | Keycloak | + +For instructions on generating these files, refer to the [Certificate Setup](#certificate-setup) section in the main README. + +## Configuring MTLS for Keycloak + +Keycloak's MTLS configuration is defined in the `docker/docker-compose.yml` file. The following environment variables control MTLS behavior: + +```yaml +KC_HTTPS_CLIENT_AUTH: ${KC_HTTPS_CLIENT_AUTH} # Set to 'required' to enforce MTLS +KC_HTTPS_ENABLED: ${KC_HTTPS_ENABLED} # Must be 'true' for MTLS +KC_HTTPS_PORT: ${KC_HTTPS_PORT} # Default is 8443 +KC_HTTPS_KEY_STORE_FILE: /cert/keystore.jks # Server certificate +KC_HTTPS_KEY_STORE_PASSWORD: ${KC_HTTPS_KEY_STORE_PASSWORD} +KC_HTTPS_CERTIFICATE_FILE: /cert/localhost.crt +KC_HTTPS_CERTIFICATE_KEY_FILE: /key/localhost.key +KC_HTTPS_TRUST_STORE_FILE: /keystores/localhost.p12 +KC_HTTPS_TRUST_STORE_PASSWORD: ${KC_HTTPS_TRUST_STORE_PASSWORD} +KC_SPI_TRUSTSTORE_FILE_FILE: /cert/truststore.jks +KC_SPI_TRUSTSTORE_FILE_PASSWORD: ${KC_SPI_TRUSTSTORE_FILE_PASSWORD} +``` + +### Steps to Configure Keycloak MTLS: + +1. Place your certificate files in the `docker` directory: + - `keystore.jks` + - `truststore.jks` + - `localhost.p12` + - `localhost.crt` + - `localhost.key` + +2. Create or update the `.env` file in the `docker` directory with the following MTLS-related variables: + ``` + KC_HTTPS_CLIENT_AUTH=required + KC_HTTPS_ENABLED=true + KC_HTTPS_PORT=8443 + KC_HTTPS_KEY_STORE_PASSWORD=changeit + KC_HTTPS_TRUST_STORE_PASSWORD=changeit + KC_SPI_TRUSTSTORE_FILE_PASSWORD=changeit + ``` + +3. The `docker-compose.yml` file maps these certificate files into the Keycloak container: + ```yaml + volumes: + - ./localhost.p12:/keystores/localhost.p12 + - ./localhost.crt:/cert/localhost.crt + - ./localhost.key:/key/localhost.key + - ./keystore.jks:/cert/keystore.jks + - ./truststore.jks:/cert/truststore.jks + ``` + +## Configuring MTLS for the Management Node + +The Management Node's MTLS configuration is defined in the `src/main/resources/application.yml` file under the `server.ssl` section: + +```yaml +server: + port: 8090 + ssl: + key-alias: localhost + key-store: /path/to/keystore.jks + key-store-type: JKS + key-store-password: changeit + trust-store: /path/to/truststore.jks + trust-store-password: changeit + trust-store-type: JKS + client-auth: need # This enables MTLS +``` + +### Steps to Configure Management Node MTLS: + +1. Update the `application.yml` file with the correct paths to your certificate files: + ```yaml + server: + port: 8090 + ssl: + key-alias: localhost + key-store: /path/to/keystore.jks # Update this path + key-store-type: JKS + key-store-password: changeit + trust-store: /path/to/truststore.jks # Update this path + trust-store-password: changeit + trust-store-type: JKS + client-auth: need # Add this line to enable MTLS + ``` + +2. For Docker deployment, update the Dockerfile to copy the certificate files: + ```dockerfile + COPY ../docker/keystore.jks /app/docker/keystore.jks + COPY ../docker/truststore.jks /app/docker/truststore.jks + ``` + +3. When running the application, ensure the certificate files are accessible at the paths specified in the configuration. + +## Testing MTLS Configuration + +To verify that MTLS is properly configured: + +1. For Keycloak: + ```bash + # This should fail without a client certificate + curl -k https://localhost:8443/health + + # This should succeed with a client certificate + curl -k --cert client.crt --key client.key https://localhost:8443/health + ``` + +2. For Management Node: + ```bash + # This should fail without a client certificate + curl -k https://localhost:8090/actuator/health + + # This should succeed with a client certificate + curl -k --cert client.crt --key client.key https://localhost:8090/actuator/health + ``` + +## Troubleshooting MTLS Issues + +Common MTLS configuration issues: + +1. **Certificate Path Issues**: + - Ensure the paths to certificate files are correct and accessible + - For Docker deployments, verify that volumes are properly mounted + +2. **Certificate Password Issues**: + - Verify that the passwords in configuration files match the actual certificate passwords + +3. **Certificate Trust Issues**: + - Ensure the client's certificate is trusted by the server's truststore + - Ensure the server's certificate is trusted by the client's truststore + +4. **Certificate Expiration**: + - Check that certificates are not expired + +For more detailed troubleshooting, check the logs: +- Keycloak logs: `docker logs keycloak` +- Management Node logs: Check the application logs for SSL-related errors \ No newline at end of file diff --git a/docs/entity-dto-converter-pattern.md b/docs/entity-dto-converter-pattern.md new file mode 100644 index 0000000..62b4998 --- /dev/null +++ b/docs/entity-dto-converter-pattern.md @@ -0,0 +1,140 @@ +# Entity-DTO Converter Pattern + +## Overview + +This document describes the Entity-DTO converter pattern implemented in the project to handle conversions between entity objects and DTOs (Data Transfer Objects). This pattern replaces the previous approach of using ModelMapper for these conversions. + +## Benefits + +- **Separation of Concerns**: Each converter is responsible for a specific entity-DTO pair, making the code more modular and easier to maintain. +- **Type Safety**: Converters provide type-safe conversions, reducing the risk of runtime errors. +- **Explicit Mapping**: Mappings between entities and DTOs are explicitly defined, making the code more readable and easier to debug. +- **Testability**: Converters can be easily unit tested in isolation. +- **Performance**: Custom converters can be more performant than reflection-based mapping libraries like ModelMapper. + +## Implementation + +### EntityDtoConverter Interface + +The `EntityDtoConverter` interface defines the contract for all entity-DTO converters: + +```java +public interface EntityDtoConverter { + D toDto(E entity); + E toEntity(D dto); + + default List toDtoList(List entities) { + if (entities == null) { + return List.of(); + } + return entities.stream() + .map(this::toDto) + .collect(Collectors.toList()); + } + + default List toEntityList(List dtos) { + if (dtos == null) { + return List.of(); + } + return dtos.stream() + .map(this::toEntity) + .collect(Collectors.toList()); + } +} +``` + +### Concrete Converter Implementations + +Concrete converter implementations are located in the `uk.gov.dbt.ndtp.ia.node.management.converter.impl` package. Each converter: + +1. Implements the `EntityDtoConverter` interface for a specific entity-DTO pair +2. Is annotated with `@Component` for Spring dependency injection +3. Handles null values safely +4. Resolves entity relationships as needed + +Example: + +```java +@Component +public class OrganisationProducerConverter implements EntityDtoConverter { + private final OrganisationRepository organisationRepository; + + public OrganisationProducerConverter(OrganisationRepository organisationRepository) { + this.organisationRepository = organisationRepository; + } + + @Override + public OrganisationProducerDTO toDto(OrganisationProducer entity) { + if (entity == null) { + return null; + } + + return OrganisationProducerDTO.builder() + .id(entity.getId()) + .name(entity.getName()) + .orgId(entity.getOrg() != null ? entity.getOrg().getId() : null) + // ... other fields + .build(); + } + + @Override + public OrganisationProducer toEntity(OrganisationProducerDTO dto) { + if (dto == null) { + return null; + } + + OrganisationProducer entity = new OrganisationProducer(); + entity.setId(dto.getId()); + entity.setName(dto.getName()); + + // Resolve relationships + if (dto.getOrgId() != null) { + Organisation organisation = organisationRepository.findById(dto.getOrgId()) + .orElse(null); + entity.setOrg(organisation); + } + + // ... other fields + + return entity; + } +} +``` + +## Usage in Services + +Service implementations use the converters for entity-DTO conversions: + +```java +@Service +public class OrganisationProducerServiceImpl implements OrganisationProducerService { + private final OrganisationProducerRepository organisationProducerRepository; + private final OrganisationProducerConverter organisationProducerConverter; + + public OrganisationProducerServiceImpl( + OrganisationProducerRepository organisationProducerRepository, + OrganisationProducerConverter organisationProducerConverter) { + this.organisationProducerRepository = organisationProducerRepository; + this.organisationProducerConverter = organisationProducerConverter; + } + + @Override + public List getProducers(List producerIds) { + List producers = organisationProducerRepository.findByIds(producerIds); + return organisationProducerConverter.toDtoList(producers); + } +} +``` + +## Best Practices + +1. **Null Safety**: Always check for null values in converter methods. +2. **Relationship Handling**: Use repository dependencies to resolve entity relationships in `toEntity` methods. +3. **Builder Pattern**: Use the builder pattern for DTO creation when available. +4. **List Conversions**: Use the default `toDtoList` and `toEntityList` methods for list conversions. +5. **Documentation**: Document any non-trivial mappings or special handling in the converter methods. +6. **Testing**: Write unit tests for converters to ensure correct mapping behavior. + +## ModelMapper + +ModelMapper is still available in the application for backward compatibility and other potential uses, but it's no longer used for entity-DTO conversions. The `ModelMapperConfig` class provides a basic ModelMapper bean with STRICT matching strategy. \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..19529dd --- /dev/null +++ b/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..249bdf3 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..71ae87f --- /dev/null +++ b/pom.xml @@ -0,0 +1,241 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.4 + + + uk.gov.dbt.ndtp.ia.management.node + management-node + 0.0.1 + management-node + Provides Management capabilities over IA Node Net + + + + + + + + + + + + + + + + 21 + 2025.0.0 + 42.7.7 + 11.10.4 + + + + + org.modelmapper + modelmapper + 3.2.0 + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.postgresql + postgresql + ${postgresql.version} + + + org.flywaydb + flyway-core + ${flyway.version} + + + org.flywaydb + flyway-database-postgresql + ${flyway.version} + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + + org.springframework.security + spring-security-test + test + + + org.mockito + mockito-core + 5.10.0 + test + + + org.mockito + mockito-junit-jupiter + 5.10.0 + test + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + true + + + + org.jacoco + jacoco-maven-plugin + 0.8.11 + + + **/dto/** + **/entity/** + **/config/** + **/exception/** + **/ManagementNodeApplication.java + + + + + prepare-agent + + prepare-agent + + + + report + test + + report + + + + check + verify + + check + + + false + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + 0.80 + + + BRANCH + COVEREDRATIO + 0.80 + + + LINE + COVEREDRATIO + 0.80 + + + METHOD + COVEREDRATIO + 0.80 + + + CLASS + COVEREDRATIO + 0.50 + + + + + + + + + + + + diff --git a/repository-configuration/.gitignore b/repository-configuration/.gitignore new file mode 100644 index 0000000..6ff38f4 --- /dev/null +++ b/repository-configuration/.gitignore @@ -0,0 +1,3 @@ +.terraform +terraform.tfstate +terraform.tfstate.backup \ No newline at end of file diff --git a/repository-configuration/README.md b/repository-configuration/README.md new file mode 100644 index 0000000..4ce83af --- /dev/null +++ b/repository-configuration/README.md @@ -0,0 +1,32 @@ +# GitHub Repository Configuration + +This directory holds [OpenTofu](https://opentofu.org/) resources for managing this repository's branch protection settings. To authenticate the GitHub provider, the following environment variable must be set to a valid access token: + +`export TF_VAR_token=<>` + +These resources apply branch protection policies consistent with the use of a [GitFlow](https://nvie.com/posts/a-successful-git-branching-model/) branching strategy. Given a repository may be in varying states of maturity, branches are not in themselves created programmatically. It is assumed develop, release/* and main branches already exist. If they do not, you can still apply these resources and later can create the target branches manually in your repository. + +A second variable that must be supplied is that of your requirement tracking system. As an example, if the link to see the original issue or requirement was to be linked to https://example.com/requirement-system/DPAV-142, you would set this variable to "https://example.com/requirement-system" using the below command: + +`export TF_VAR_requirement_tracking_url_base=https://example.com/requirement-system` + +Once environment variables have been been set, you can initialise OpenTofu by running `tofu init`, followed by `tofu apply` to apply the configuration. If you are working on an existing repository, you must import the repository and any existing protection rules using the commands in the Importing existing resources section. + +## Importing existing resources + +If you are retrospectively applying these resources to manage an existing repository, the below import commands can be used. For information about the import commands themselves please see: + +* https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository +* https://registry.terraform.io/providers/integrations/github/latest/docs/resources/branch_protection. + +``` +export REPOSITORY_NAME=<> +tofu import github_repository.repository $REPOSITORY_NAME +tofu import github_branch_protection.develop_branch_protection $REPOSITORY_NAME:develop +tofu import github_branch_protection.release_branch_protection $REPOSITORY_NAME:release/* +tofu import github_branch_protection.main_branch_protection $REPOSITORY_NAME:main +``` + + +© Crown Copyright 2025. This work has been developed by the National Digital Twin Programme and is legally attributed to the Department for Business and Trade (UK) as the governing entity. +Licensed under the Open Government Licence v3.0. diff --git a/repository-configuration/provider.tf b/repository-configuration/provider.tf new file mode 100644 index 0000000..4a0d928 --- /dev/null +++ b/repository-configuration/provider.tf @@ -0,0 +1,4 @@ +provider "github" { + token = var.token + owner = var.organisation +} \ No newline at end of file diff --git a/repository-configuration/repository.tf b/repository-configuration/repository.tf new file mode 100644 index 0000000..7d6e41c --- /dev/null +++ b/repository-configuration/repository.tf @@ -0,0 +1,50 @@ +resource "github_repository" "repository" { + name = basename(dirname(path.cwd)) + description = var.repository_description + visibility = "public" + delete_branch_on_merge = false + has_downloads = true + has_issues = true + has_projects = true +} + +resource "github_branch_protection" "develop_branch_protection" { + repository_id = github_repository.repository.node_id + pattern = "develop" + + required_pull_request_reviews { + required_approving_review_count = 1 + } + + required_status_checks { + contexts = [] + strict = true + } +} + +resource "github_branch_protection" "release_branch_protection" { + repository_id = github_repository.repository.node_id + pattern = "release/*" +} + +resource "github_branch_protection" "main_branch_protection" { + repository_id = github_repository.repository.node_id + pattern = "main" + + required_pull_request_reviews { + required_approving_review_count = 1 + } + + required_status_checks { + contexts = [] + strict = true + } +} + +# Autolink references +resource "github_repository_autolink_reference" "autolink" { + repository = github_repository.repository.name + + key_prefix = "${var.requirement_tracking_id}-" + target_url_template = "${var.requirement_tracking_url_base}/${var.requirement_tracking_id}-" +} diff --git a/repository-configuration/terraform.tf b/repository-configuration/terraform.tf new file mode 100644 index 0000000..6c45f23 --- /dev/null +++ b/repository-configuration/terraform.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + github = { + source = "integrations/github" + version = "~> 6.0" + } + } +} \ No newline at end of file diff --git a/repository-configuration/variables.tf b/repository-configuration/variables.tf new file mode 100644 index 0000000..50fcc03 --- /dev/null +++ b/repository-configuration/variables.tf @@ -0,0 +1,28 @@ +variable "token" { + description = "GitHub personal access token." + type = string + sensitive = true +} + +variable "organisation" { + description = "The GitHub organisation name." + type = string + default = "National-Digital-Twin" +} + +variable "repository_description" { + description = "GitHub repository description." + type = string + default = "Provides Management capabilities over IA Node Net" +} + +variable "requirement_tracking_url_base" { + description = "Requirement tracking system URL base to be used for autolinking commit messages." + type = string +} + +variable "requirement_tracking_id" { + description = "Requirement identifier to be used for autolinking commit messages. This ID should match those which prefix issue identifiers, for example DPAV." + type = string + default = "DPAV" +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/ManagementNodeApplication.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/ManagementNodeApplication.java new file mode 100644 index 0000000..20d2d8b --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/ManagementNodeApplication.java @@ -0,0 +1,13 @@ +package uk.gov.dbt.ndtp.ia.node.management; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ManagementNodeApplication { + + public static void main(String[] args) { + SpringApplication.run(ManagementNodeApplication.class, args); + } + +} diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/ClientIdMdcFilter.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/ClientIdMdcFilter.java new file mode 100644 index 0000000..237bc3e --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/ClientIdMdcFilter.java @@ -0,0 +1,99 @@ +package uk.gov.dbt.ndtp.ia.node.management.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import uk.gov.dbt.ndtp.ia.node.management.model.jwt.EnhancedPrincipal; + +import java.io.IOException; + +/** + * Filter that adds the clientId from the Authentication object to the MDC context. + * This allows the clientId to be included in all log messages. + * This filter should be registered to run after the BearerTokenAuthenticationFilter + * to ensure that the Authentication object is already set in the SecurityContext. + * If the Authentication object is null or does not contain an EnhancedPrincipal, + * the filter will use "unknown" as the clientId. + */ +@Component +@Slf4j +public class ClientIdMdcFilter extends OncePerRequestFilter { + + public static final String CLIENT_ID_MDC_KEY = "clientId"; + private static final String UNKNOWN_CLIENT = ""; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + try { + log.trace("ClientIdMdcFilter processing request: {}", request.getRequestURI()); + + // Get the Authentication object from the SecurityContext + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null) { + log.trace("Authentication is null in SecurityContextHolder for request: {}", request.getRequestURI()); + } else { + log.trace("Authentication found in SecurityContextHolder: {}, Principal type: {}", + authentication.getName(), + authentication.getPrincipal() != null ? authentication.getPrincipal().getClass().getName() : "null"); + } + + // Extract clientId from the Authentication object if possible + String clientId = extractClientId(authentication); + + // Put the clientId in the MDC context + MDC.put(CLIENT_ID_MDC_KEY, clientId); + + log.trace("Added clientId to MDC: {}", clientId); + + // Continue with the filter chain + filterChain.doFilter(request, response); + } finally { + // Always clear the MDC context to prevent memory leaks + MDC.remove(CLIENT_ID_MDC_KEY); + log.trace("Removed clientId from MDC"); + } + } + + /** + * Extracts the clientId from the Authentication object. + * + * This method checks if the Authentication object is not null and if its principal + * is an instance of EnhancedPrincipal. If so, it extracts the clientId from the + * EnhancedPrincipal. Otherwise, it returns "unknown". + * + * Debug logging is included to help diagnose issues with the Authentication object + * and its principal. + * + * @param authentication the Authentication object + * @return the clientId, or "unknown" if it cannot be determined + */ + private String extractClientId(Authentication authentication) { + if (authentication != null) { + log.trace("Authentication found: {}, Principal type: {}", + authentication.getName(), + authentication.getPrincipal() != null ? authentication.getPrincipal().getClass().getName() : "null"); + + Object principal = authentication.getPrincipal(); + + if (principal instanceof EnhancedPrincipal enhancedPrincipal) { + String clientId = enhancedPrincipal.getClientId(); + return clientId != null && !clientId.isEmpty() ? clientId : UNKNOWN_CLIENT; + } else { + log.warn("Principal is not an instance of EnhancedPrincipal: {}", + principal != null ? principal.getClass().getName() : "null"); + } + } else { + log.trace("Authentication is null in SecurityContextHolder"); + } + return UNKNOWN_CLIENT; + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/CustomJwtAuthenticationToken.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/CustomJwtAuthenticationToken.java new file mode 100644 index 0000000..212742f --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/CustomJwtAuthenticationToken.java @@ -0,0 +1,34 @@ +package uk.gov.dbt.ndtp.ia.node.management.config; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import uk.gov.dbt.ndtp.ia.node.management.model.jwt.EnhancedPrincipal; + +import java.util.Collection; + +/** + * Custom JWT Authentication Token that uses CustomPrincipal as the principal object. + * This allows access to the clientId in addition to the standard JWT information. + */ +public class CustomJwtAuthenticationToken extends JwtAuthenticationToken { + + private final EnhancedPrincipal principal; + + /** + * Constructs a CustomJwtAuthenticationToken with the provided JWT, authorities, and CustomPrincipal. + * + * @param jwt the JWT + * @param authorities the collection of granted authorities + * @param principal the custom principal containing subject and clientId + */ + public CustomJwtAuthenticationToken(Jwt jwt, Collection authorities, EnhancedPrincipal principal) { + super(jwt, authorities, principal.getSubject()); + this.principal = principal; + } + + @Override + public EnhancedPrincipal getPrincipal() { + return this.principal; + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/KeycloakJwtAuthenticationConverter.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/KeycloakJwtAuthenticationConverter.java new file mode 100644 index 0000000..b138702 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/KeycloakJwtAuthenticationConverter.java @@ -0,0 +1,337 @@ +package uk.gov.dbt.ndtp.ia.node.management.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import uk.gov.dbt.ndtp.ia.node.management.exception.ResourceAccessParsingException; +import uk.gov.dbt.ndtp.ia.node.management.exception.TokenIntrospectionException; +import uk.gov.dbt.ndtp.ia.node.management.model.jwt.EnhancedPrincipal; +import uk.gov.dbt.ndtp.ia.node.management.model.jwt.JwtToken; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * The KeycloakJwtAuthenticationConverter class is responsible for converting a JWT token + * into an authentication token by integrating with Keycloak-specific token introspection + * and resource access information. This class ensures that roles, client ID, and other + * relevant token claims are parsed accurately to produce an appropriate authentication + * object. + * + * This class implements the {@link Converter} interface to provide conversion functionality + * between a {@link Jwt} and an instance of {@link AbstractAuthenticationToken}. + * + * Key responsibilities include: + * - Performing token introspection via the configured Keycloak introspection endpoint. + * - Extracting authorities, roles, and resource access data from the token. + * - Handling potential fallback scenarios when introspection fails. + * - Processing key JWT claims such as "azp", "client_id", "sub", and resource-access roles. + * + * Introspection is handled through HTTP calls using {@link RestTemplate} to ensure that + * the token's validity and associated claims are verified against the configured Keycloak + * server. + * + * Thread Safety: + * This class is designed to be thread-safe as it does not maintain mutable state at + * the instance level. Configuration properties are injected as immutable fields and are + * not modified during token conversion operations. + */ +@Component +@Slf4j + +public class KeycloakJwtAuthenticationConverter implements Converter { + // Constants for claim names + private static final String CLAIM_AZP = "azp"; + private static final String CLAIM_CLIENT_ID = "client_id"; + private static final String CLAIM_RESOURCE_ACCESS = "resource_access"; + private static final String CLAIM_REALM_ACCESS = "realm_access"; + private static final String CLAIM_ROLES = "roles"; + private static final String CLAIM_SUB = "sub"; + private static final String CLAIM_ACTIVE = "active"; + + // Constants for role prefixes and default values + private static final String ROLE_PREFIX = "ROLE_"; + private static final String UNKNOWN_CLIENT = "unknown"; + private static final String RESOURCE_ROLE_SEPARATOR = ":"; + + // Form data keys + private static final String FORM_CLIENT_ID = "client_id"; + private static final String FORM_CLIENT_SECRET = "client_secret"; + private static final String FORM_TOKEN = "token"; + + private final JwtGrantedAuthoritiesConverter defaultGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + private final RestTemplate restTemplate = new RestTemplate(); + + @Value("${spring.security.oauth2.resourceserver.opaquetoken.introspection-uri}") + private String introspectionUri; + + @Value("${spring.security.oauth2.resourceserver.opaquetoken.client-id}") + private String clientId; + + @Value("${spring.security.oauth2.resourceserver.opaquetoken.client-secret}") + private String clientSecret; + + /** + * Performs token introspection by making an HTTP call to the introspection endpoint. + * + * @param tokenValue The JWT token value to introspect + * @return JwtToken containing the introspection data + * @throws TokenIntrospectionException If the introspection request fails or returns invalid data + */ + private JwtToken performTokenIntrospection(String tokenValue) throws TokenIntrospectionException { + try { + // Prepare headers for the introspection request + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + // Prepare form data for the introspection request + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add(FORM_CLIENT_ID, clientId); + formData.add(FORM_CLIENT_SECRET, clientSecret); + formData.add(FORM_TOKEN, tokenValue); + + // Create the request entity + HttpEntity> requestEntity = new HttpEntity<>(formData, headers); + + log.debug("Performing token introspection for client ID: {}", clientId); + + // Make the introspection request + ResponseEntity response = restTemplate.postForEntity(introspectionUri, requestEntity, JwtToken.class); + + // Parse the response + JwtToken introspectionData = response.getBody(); + + // Check if token is active + if (introspectionData == null || !Boolean.TRUE.equals(introspectionData.getActive())) { + log.error("Token introspection failed: Token is not active for client ID: {}", clientId); + throw new TokenIntrospectionException("Token is not active", clientId); + } + + return introspectionData; + } catch (TokenIntrospectionException e) { + // Re-throw TokenIntrospectionException + throw e; + } catch (Exception e) { + log.error("Token introspection failed for client ID: {}", clientId, e); + throw new TokenIntrospectionException("Failed to introspect token", e, clientId); + } + } + + @Override + public AbstractAuthenticationToken convert(Jwt jwt) { + try { + log.debug("Converting JWT to authentication token"); + + // Perform token introspection + JwtToken introspectionData = performTokenIntrospection(jwt.getTokenValue()); + + // Extract authorities from the introspection data + Collection authorities = extractAuthoritiesFromIntrospection(introspectionData); + + // Extract client_id from introspection data + String tokenClientId = extractClientIdFromIntrospection(introspectionData); + + // Extract subject from introspection data + String subject = introspectionData.getSub(); + if (subject == null || subject.isEmpty()) { + subject = jwt.getSubject(); // Fallback to JWT subject if not in introspection data + } + + // Create custom principal with subject and clientId + EnhancedPrincipal principal = new EnhancedPrincipal(subject, tokenClientId); + + log.debug("Successfully created authentication token for client ID: {}", tokenClientId); + + // Return custom authentication token + return new CustomJwtAuthenticationToken(jwt, authorities, principal); + } catch (TokenIntrospectionException e) { + // If introspection fails, log the error and fall back to JWT parsing + String tokenClientId = extractClientId(jwt); + log.warn("Token introspection failed for client ID: {}. Falling back to JWT parsing. Error: {}", tokenClientId, e.getMessage()); + + Collection authorities = extractAuthorities(jwt); + EnhancedPrincipal principal = new EnhancedPrincipal(jwt.getSubject(), tokenClientId); + return new CustomJwtAuthenticationToken(jwt, authorities, principal); + } catch (ResourceAccessParsingException e) { + // If resource access parsing fails, log the error and fall back to JWT parsing + String tokenClientId = extractClientId(jwt); + log.warn("Resource access parsing failed for client ID: {}. Falling back to JWT parsing. Error: {}", tokenClientId, e.getMessage()); + + Collection authorities = extractAuthorities(jwt); + EnhancedPrincipal principal = new EnhancedPrincipal(jwt.getSubject(), tokenClientId); + return new CustomJwtAuthenticationToken(jwt, authorities, principal); + } catch (Exception e) { + // For any other unexpected exceptions + String tokenClientId = extractClientId(jwt); + log.error("Unexpected error during JWT conversion for client ID: {}", tokenClientId, e); + + Collection authorities = extractAuthorities(jwt); + EnhancedPrincipal principal = new EnhancedPrincipal(jwt.getSubject(), tokenClientId); + return new CustomJwtAuthenticationToken(jwt, authorities, principal); + } + } + + /** + * Helper method to get a non-null, non-empty client ID from primary and fallback sources. + * + * @param primaryId The primary client ID to check + * @param fallbackId The fallback client ID to use if primary is null or empty + * @return A non-null client ID (either primary, fallback, or "unknown") + */ + private String getEffectiveClientId(String primaryId, String fallbackId) { + if (primaryId != null && !primaryId.isEmpty()) { + return primaryId; + } + + if (fallbackId != null && !fallbackId.isEmpty()) { + return fallbackId; + } + + return UNKNOWN_CLIENT; + } + + /** + * Extract client_id from JWT token. + * Tries to get it from "azp" claim first, then from "client_id" claim. + * If neither is present, returns "unknown". + */ + private String extractClientId(Jwt jwt) { + String azpClientId = jwt.getClaimAsString(CLAIM_AZP); + String directClientId = jwt.getClaimAsString(CLAIM_CLIENT_ID); + + return getEffectiveClientId(azpClientId, directClientId); + } + + /** + * Extract authorities from introspection data. + * + * @param jwtToken The data from the introspection endpoint + * @return Collection of GrantedAuthority objects + */ + private Collection extractAuthoritiesFromIntrospection(JwtToken jwtToken) { + Collection authorities = new ArrayList<>(); + String clientId = extractClientIdFromIntrospection(jwtToken); + + try { + log.debug("Extracting authorities from introspection data for client ID: {}", clientId); + + // Process resource_access + if (jwtToken.getResourceAccess() != null) { + jwtToken.getResourceAccess().forEach((resource, resourceAccess) -> { + if (resourceAccess != null && resourceAccess.getRoles() != null) { + resourceAccess.getRoles().forEach(role -> { + authorities.add(new SimpleGrantedAuthority(ROLE_PREFIX + resource + RESOURCE_ROLE_SEPARATOR + role)); + }); + } + }); + } + + log.trace("Successfully extracted {} authorities for client ID: {}", authorities.size(), clientId); + } catch (Exception e) { + log.error("Error extracting authorities from introspection data for client ID: {}", clientId, e); + throw new ResourceAccessParsingException("Failed to parse resource access from introspection data", e, clientId); + } + + return authorities; + } + + /** + * Extract client_id from introspection data. + * Tries to get it from "azp" claim first, then from "client_id" claim. + * If neither is present, returns "unknown". + * + * @param jwtToken The data from the introspection endpoint + * @return The client ID + */ + private String extractClientIdFromIntrospection(JwtToken jwtToken) { + String azpClientId = jwtToken.getAzp(); + String directClientId = jwtToken.getClientId(); + + return getEffectiveClientId(azpClientId, directClientId); + } + + /** + * Safely extracts a Map from an Object, if the object is a Map. + * + * @param obj The object to extract a Map from + * @return An Optional containing the Map if extraction was successful, or empty Optional otherwise + */ + @SuppressWarnings("unchecked") // Safe cast with instanceof check + private Optional> extractMap(Object obj) { + if (obj instanceof Map) { + return Optional.of((Map) obj); + } + return Optional.empty(); + } + + /** + * Safely extracts a Collection of Strings from an Object, if the object is a Collection. + * + * @param obj The object to extract a Collection from + * @return An Optional containing the Collection if extraction was successful, or empty Optional otherwise + */ + @SuppressWarnings("unchecked") // Safe cast with instanceof check + private Optional> extractStringCollection(Object obj) { + if (obj instanceof Collection) { + return Optional.of((Collection) obj); + } + return Optional.empty(); + } + + /** + * Processes resource roles and creates authorities. + * + * @param resourceName The name of the resource + * @param resourceData The resource data containing roles + * @return A collection of GrantedAuthority objects + */ + private Collection processResourceRoles(String resourceName, Map resourceData) { + if (!resourceData.containsKey(CLAIM_ROLES)) { + return Collections.emptyList(); + } + + return extractStringCollection(resourceData.get(CLAIM_ROLES)).map(roles -> roles.stream().map(role -> { + if (resourceName != null) { + // Resource-specific role (format: ROLE_:) + return new SimpleGrantedAuthority(ROLE_PREFIX + resourceName + RESOURCE_ROLE_SEPARATOR + role); + } else { + // Realm role (format: ROLE_) + return new SimpleGrantedAuthority(ROLE_PREFIX + role); + } + }).map(authority -> authority).collect(Collectors.toList())).orElse(Collections.emptyList()); + } + + private Collection extractAuthorities(Jwt jwt) { + // Add default authorities if any + Collection authorities = new ArrayList<>(defaultGrantedAuthoritiesConverter.convert(jwt)); + String clientId = extractClientId(jwt); + + try { + log.trace("Extracting authorities from JWT for client ID: {}", clientId); + + // Extract and process resource_access claim + extractMap(jwt.getClaim(CLAIM_RESOURCE_ACCESS)).ifPresent(resourceAccess -> resourceAccess.forEach((resource, resourceDataObj) -> extractMap(resourceDataObj).ifPresent(resourceData -> authorities.addAll(processResourceRoles(resource, resourceData))))); + + log.trace("Successfully extracted {} authorities from JWT for client ID: {}", authorities.size(), clientId); + } catch (Exception e) { + log.error("Error extracting authorities from JWT for client ID: {}", clientId, e); + // We're not throwing the exception here because we want to continue with default authorities + // This is a fallback method, so we want to be more lenient + } + + return authorities; + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/ModelMapperConfig.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/ModelMapperConfig.java new file mode 100644 index 0000000..b27712f --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/ModelMapperConfig.java @@ -0,0 +1,27 @@ +package uk.gov.dbt.ndtp.ia.node.management.config; + +import org.modelmapper.ModelMapper; +import org.modelmapper.convention.MatchingStrategies; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration for ModelMapper. + * Note: Entity-DTO conversions are now handled by dedicated converter classes in the converter package. + * ModelMapper is kept for backward compatibility and other potential uses. + */ +@Configuration +public class ModelMapperConfig { + + /** + * Creates a ModelMapper bean with basic configuration. + * + * @return the configured ModelMapper + */ + @Bean + public ModelMapper modelMapper() { + ModelMapper modelMapper = new ModelMapper(); + modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT); + return modelMapper; + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/SecurityConfig.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/SecurityConfig.java new file mode 100644 index 0000000..4882439 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/SecurityConfig.java @@ -0,0 +1,46 @@ +package uk.gov.dbt.ndtp.ia.node.management.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter; + private final ClientIdMdcFilter clientIdMdcFilter; + + public SecurityConfig(KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter, + ClientIdMdcFilter clientIdMdcFilter) { + this.keycloakJwtAuthenticationConverter = keycloakJwtAuthenticationConverter; + this.clientIdMdcFilter = clientIdMdcFilter; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/actuator/**").permitAll() + //.requestMatchers("/api/v1/configuration/**").permitAll() + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt.jwtAuthenticationConverter(keycloakJwtAuthenticationConverter)) + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + // Add ClientIdMdcFilter after the BearerTokenAuthenticationFilter + // This ensures the Authentication object is already set in the SecurityContext + .addFilterAfter(clientIdMdcFilter, BearerTokenAuthenticationFilter.class); + + return http.build(); + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/SslPropertyInitializer.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/SslPropertyInitializer.java new file mode 100644 index 0000000..5b6cc38 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/SslPropertyInitializer.java @@ -0,0 +1,38 @@ +package uk.gov.dbt.ndtp.ia.node.management.config; + +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class SslPropertyInitializer { + + @Value("${server.ssl.key-store}") + private String keyStore; + + @Value("${server.ssl.key-store-password}") + private String keyStorePassword; + + @Value("${server.ssl.key-store-type:JKS}") + private String keyStoreType; + + @Value("${server.ssl.trust-store}") + private String trustStore; + + @Value("${server.ssl.trust-store-password}") + private String trustStorePassword; + + @Value("${server.ssl.trust-store-type:JKS}") + private String trustStoreType; + + @PostConstruct + public void init() { + System.setProperty("javax.net.ssl.keyStore", keyStore); + System.setProperty("javax.net.ssl.keyStorePassword", keyStorePassword); + System.setProperty("javax.net.ssl.keyStoreType", keyStoreType); + + System.setProperty("javax.net.ssl.trustStore", trustStore); + System.setProperty("javax.net.ssl.trustStorePassword", trustStorePassword); + System.setProperty("javax.net.ssl.trustStoreType", trustStoreType); + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/controller/v1/ConfigurationController.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/controller/v1/ConfigurationController.java new file mode 100644 index 0000000..71ebf3d --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/controller/v1/ConfigurationController.java @@ -0,0 +1,53 @@ +package uk.gov.dbt.ndtp.ia.node.management.controller.v1; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ConsumerConfigDTO; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProducerConfigDTO; +import uk.gov.dbt.ndtp.ia.node.management.model.jwt.EnhancedPrincipal; +import uk.gov.dbt.ndtp.ia.node.management.service.providers.configuration.ConfigurationProvider; + +import java.util.Optional; + +@RestController +@RequestMapping("/api/v1/configuration") +@Slf4j +public class ConfigurationController { + + private final ConfigurationProvider configurationProvider; + + public ConfigurationController(ConfigurationProvider configurationProvider) { + this.configurationProvider = configurationProvider; + } + + @GetMapping("/producer") + @PreAuthorize("hasRole('ROLE_management-node:access_producer_configurations')") + public ProducerConfigDTO getProducerConfigurations( + @AuthenticationPrincipal EnhancedPrincipal principal, + @RequestParam(value = "producer_id", required = false) Long producer_id) { + log.info("Preparing Producer Config for producer {}", producer_id); + return configurationProvider.getProducerConfigByClientId( + principal.getClientId(), producer_id != null ? Optional.of(producer_id) : Optional.empty()); + } + + @GetMapping("/consumer") + @PreAuthorize("hasRole('ROLE_management-node:access_consumer_configurations')") + public ConsumerConfigDTO getConsumerConfigurations( + @AuthenticationPrincipal EnhancedPrincipal principal, + @RequestParam(value = "consumer_id", required = false) Long consumerId) { + log.info( + "Preparing Consumer Config for client Id {} and Consumer {}", + principal.getClientId(), + consumerId); + + return configurationProvider.getConsumerConfigByClientId( + principal.getClientId(), consumerId != null ? Optional.of(consumerId) : Optional.empty()); + } + + +} diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/EntityDtoConverter.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/EntityDtoConverter.java new file mode 100644 index 0000000..f72b20d --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/EntityDtoConverter.java @@ -0,0 +1,59 @@ +package uk.gov.dbt.ndtp.ia.node.management.converter; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Generic interface for converting between entity and DTO objects. + * + * @param the entity type + * @param the DTO type + */ +public interface EntityDtoConverter { + + /** + * Converts an entity to a DTO. + * + * @param entity the entity to convert + * @return the converted DTO + */ + D toDto(E entity); + + /** + * Converts a DTO to an entity. + * + * @param dto the DTO to convert + * @return the converted entity + */ + E toEntity(D dto); + + /** + * Converts a list of entities to a list of DTOs. + * + * @param entities the entities to convert + * @return the converted DTOs + */ + default List toDtoList(List entities) { + if (entities == null) { + return List.of(); + } + return entities.stream() + .map(this::toDto) + .collect(Collectors.toList()); + } + + /** + * Converts a list of DTOs to a list of entities. + * + * @param dtos the DTOs to convert + * @return the converted entities + */ + default List toEntityList(List dtos) { + if (dtos == null) { + return List.of(); + } + return dtos.stream() + .map(this::toEntity) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ConsumerConverter.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ConsumerConverter.java new file mode 100644 index 0000000..698c377 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ConsumerConverter.java @@ -0,0 +1,73 @@ +package uk.gov.dbt.ndtp.ia.node.management.converter.impl; + +import org.springframework.stereotype.Component; +import uk.gov.dbt.ndtp.ia.node.management.converter.EntityDtoConverter; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ConsumerDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Organisation; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Consumer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.OrganisationRepository; + +/** + * Converter for ConsumerId entity and ConsumerIdDTO. + */ +@Component +public class ConsumerConverter implements EntityDtoConverter { + + private final OrganisationRepository organisationRepository; + + /** + * Constructor-based dependency injection. + * + * @param organisationRepository the organisation repository + */ + public ConsumerConverter(OrganisationRepository organisationRepository) { + this.organisationRepository = organisationRepository; + } + + /** + * Converts an ConsumerId entity to an ConsumerIdDTO. + * + * @param entity the entity to convert + * @return the converted DTO + */ + @Override + public ConsumerDTO toDto(Consumer entity) { + if (entity == null) { + return null; + } + + return ConsumerDTO.builder() + .id(entity.getId()) + .name(entity.getName()) + .orgId(entity.getOrg() != null ? entity.getOrg().getId() : null) + .idpClientId(entity.getIdpClientId()) + .build(); + } + + /** + * Converts an ConsumerIdDTO to an ConsumerId entity. + * + * @param dto the DTO to convert + * @return the converted entity + */ + @Override + public Consumer toEntity(ConsumerDTO dto) { + if (dto == null) { + return null; + } + + Consumer entity = new Consumer(); + entity.setId(dto.getId()); + entity.setName(dto.getName()); + entity.setIdpClientId(dto.getIdpClientId()); + + // Set the organisation if orgId is provided + if (dto.getOrgId() != null) { + Organisation organisation = organisationRepository.findById(dto.getOrgId()) + .orElse(null); + entity.setOrg(organisation); + } + + return entity; + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/OrganisationProducerConverter.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/OrganisationProducerConverter.java new file mode 100644 index 0000000..fd08df7 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/OrganisationProducerConverter.java @@ -0,0 +1,116 @@ +package uk.gov.dbt.ndtp.ia.node.management.converter.impl; + +import org.springframework.stereotype.Component; +import uk.gov.dbt.ndtp.ia.node.management.converter.EntityDtoConverter; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProducerDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Product; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Organisation; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Producer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.OrganisationRepository; + +import java.util.ArrayList; +import java.util.List; + +/** + * Converter for OrganisationProducer entity and OrganisationProducerDTO. + */ +@Component +public class OrganisationProducerConverter implements EntityDtoConverter { + + private final OrganisationRepository organisationRepository; + private final ProductConverter productConverter; + + /** + * Constructor-based dependency injection. + * + * @param organisationRepository the organisation repository + * @param productConverter the data provider converter + */ + public OrganisationProducerConverter(OrganisationRepository organisationRepository, + ProductConverter productConverter) { + this.organisationRepository = organisationRepository; + this.productConverter = productConverter; + } + + /** + * Converts an OrganisationProducer entity to an OrganisationProducerDTO. + * + * @param entity the entity to convert + * @return the converted DTO + */ + @Override + public ProducerDTO toDto(Producer entity) { + if (entity == null) { + return null; + } + + ProducerDTO dto = ProducerDTO.builder() + .id(entity.getId()) + .name(entity.getName()) + .description(entity.getDescription()) + .orgId(entity.getOrg() != null ? entity.getOrg().getId() : null) + .active(entity.getActive()) + .host(entity.getHost()) + .port(entity.getPort()) + .tls(entity.getTls()) + .idpClientId(entity.getIdpClientId()) + .build(); + + // Map dataProviders if they exist + if (entity.getProducts() != null && !entity.getProducts().isEmpty()) { + entity.getProducts().forEach(dataProvider -> + dto.getDataProviders().add(productConverter.toDto(dataProvider))); + } + + return dto; + } + + /** + * Converts an OrganisationProducerDTO to an OrganisationProducer entity. + * + * @param dto the DTO to convert + * @return the converted entity + */ + @Override + public Producer toEntity(ProducerDTO dto) { + if (dto == null) { + return null; + } + + Producer entity = new Producer(); + entity.setId(dto.getId()); + entity.setName(dto.getName()); + entity.setDescription(dto.getDescription()); + entity.setActive(dto.getActive()); + entity.setHost(dto.getHost()); + entity.setPort(dto.getPort()); + entity.setTls(dto.getTls()); + entity.setIdpClientId(dto.getIdpClientId()); + + // Set the organisation if orgId is provided + if (dto.getOrgId() != null) { + Organisation organisation = organisationRepository.findById(dto.getOrgId()) + .orElse(null); + entity.setOrg(organisation); + } + + // Map dataProviders if they exist + if (dto.getDataProviders() != null && !dto.getDataProviders().isEmpty()) { + List dataProviders = new ArrayList<>(); + dto.getDataProviders().forEach(dataProviderDTO -> { + // Set the producerId to ensure proper mapping + if (dataProviderDTO.getProducerId() == null && dto.getId() != null) { + dataProviderDTO.setProducerId(dto.getId()); + } + Product dataProvider = productConverter.toEntity(dataProviderDTO); + if (dataProvider != null) { + dataProvider.setProducer(entity); + dataProviders.add(dataProvider); + } + }); + entity.setProducts(dataProviders); + } + + return entity; + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProducerConverter.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProducerConverter.java new file mode 100644 index 0000000..2b9d96e --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProducerConverter.java @@ -0,0 +1,116 @@ +package uk.gov.dbt.ndtp.ia.node.management.converter.impl; + +import org.springframework.stereotype.Component; +import uk.gov.dbt.ndtp.ia.node.management.converter.EntityDtoConverter; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProducerDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Product; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Organisation; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Producer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.OrganisationRepository; + +import java.util.ArrayList; +import java.util.List; + +/** + * Converter for Producer entity and ProducerDTO. + */ +@Component +public class ProducerConverter implements EntityDtoConverter { + + private final OrganisationRepository organisationRepository; + private final ProductConverter productConverter; + + /** + * Constructor-based dependency injection. + * + * @param organisationRepository the organisation repository + * @param productConverter the data provider converter + */ + public ProducerConverter(OrganisationRepository organisationRepository, + ProductConverter productConverter) { + this.organisationRepository = organisationRepository; + this.productConverter = productConverter; + } + + /** + * Converts a Producer entity to a ProducerDTO. + * + * @param entity the entity to convert + * @return the converted DTO + */ + @Override + public ProducerDTO toDto(Producer entity) { + if (entity == null) { + return null; + } + + ProducerDTO dto = ProducerDTO.builder() + .id(entity.getId()) + .name(entity.getName()) + .description(entity.getDescription()) + .orgId(entity.getOrg() != null ? entity.getOrg().getId() : null) + .active(entity.getActive()) + .host(entity.getHost()) + .port(entity.getPort()) + .tls(entity.getTls()) + .idpClientId(entity.getIdpClientId()) + .build(); + + // Map dataProviders if they exist + if (entity.getProducts() != null && !entity.getProducts().isEmpty()) { + entity.getProducts().forEach(dataProvider -> + dto.getDataProviders().add(productConverter.toDto(dataProvider))); + } + + return dto; + } + + /** + * Converts a ProducerDTO to a Producer entity. + * + * @param dto the DTO to convert + * @return the converted entity + */ + @Override + public Producer toEntity(ProducerDTO dto) { + if (dto == null) { + return null; + } + + Producer entity = new Producer(); + entity.setId(dto.getId()); + entity.setName(dto.getName()); + entity.setDescription(dto.getDescription()); + entity.setActive(dto.getActive()); + entity.setHost(dto.getHost()); + entity.setPort(dto.getPort()); + entity.setTls(dto.getTls()); + entity.setIdpClientId(dto.getIdpClientId()); + + // Set the organisation if orgId is provided + if (dto.getOrgId() != null) { + Organisation organisation = organisationRepository.findById(dto.getOrgId()) + .orElse(null); + entity.setOrg(organisation); + } + + // Map dataProviders if they exist + if (dto.getDataProviders() != null && !dto.getDataProviders().isEmpty()) { + List dataProviders = new ArrayList<>(); + dto.getDataProviders().forEach(dataProviderDTO -> { + // Set the producerId to ensure proper mapping + if (dataProviderDTO.getProducerId() == null && dto.getId() != null) { + dataProviderDTO.setProducerId(dto.getId()); + } + Product dataProvider = productConverter.toEntity(dataProviderDTO); + if (dataProvider != null) { + dataProvider.setProducer(entity); + dataProviders.add(dataProvider); + } + }); + entity.setProducts(dataProviders); + } + + return entity; + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProductConsumerConverter.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProductConsumerConverter.java new file mode 100644 index 0000000..0650c28 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProductConsumerConverter.java @@ -0,0 +1,60 @@ +package uk.gov.dbt.ndtp.ia.node.management.converter.impl; + +import org.springframework.stereotype.Component; +import uk.gov.dbt.ndtp.ia.node.management.converter.EntityDtoConverter; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProductConsumerDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumerId; + +/** + * Converter for ConsumerAllowedDataProvider entity and ConsumerAllowedDataProviderDTO. + */ +@Component +public class ProductConsumerConverter implements EntityDtoConverter { + + /** + * Converts a ConsumerAllowedDataProvider entity to a ConsumerAllowedDataProviderDTO. + * + * @param entity the entity to convert + * @return the converted DTO + */ + @Override + public ProductConsumerDTO toDto(ProductConsumer entity) { + if (entity == null) { + return null; + } + + return ProductConsumerDTO.builder() + .productId(entity.getId().getProductId()) + .consumerId(entity.getId().getConsumerId()) + .grantedTs(entity.getGrantedTs()) + .validity(entity.getValidity()) + .build(); + } + + /** + * Converts a ConsumerAllowedDataProviderDTO to a ConsumerAllowedDataProvider entity. + * + * @param dto the DTO to convert + * @return the converted entity + */ + @Override + public ProductConsumer toEntity(ProductConsumerDTO dto) { + if (dto == null) { + return null; + } + + ProductConsumer entity = new ProductConsumer(); + + // Create and set the embedded ID + ProductConsumerId id = new ProductConsumerId(); + id.setProductId(dto.getProductId()); + id.setConsumerId(dto.getConsumerId()); + entity.setId(id); + + entity.setGrantedTs(dto.getGrantedTs()); + entity.setValidity(dto.getValidity()); + + return entity; + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProductConverter.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProductConverter.java new file mode 100644 index 0000000..9427d5e --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProductConverter.java @@ -0,0 +1,73 @@ +package uk.gov.dbt.ndtp.ia.node.management.converter.impl; + +import org.springframework.stereotype.Component; +import uk.gov.dbt.ndtp.ia.node.management.converter.EntityDtoConverter; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProductDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Product; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Producer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.ProducerRepository; + +/** + * Converter for OrganisationDataProvider entity and OrganisationDataProviderDTO. + */ +@Component +public class ProductConverter implements EntityDtoConverter { + + private final ProducerRepository producerRepository; + + /** + * Constructor-based dependency injection. + * + * @param producerRepository the organisation producer repository + */ + public ProductConverter(ProducerRepository producerRepository) { + this.producerRepository = producerRepository; + } + + /** + * Converts an OrganisationDataProvider entity to an OrganisationDataProviderDTO. + * + * @param entity the entity to convert + * @return the converted DTO + */ + @Override + public ProductDTO toDto(Product entity) { + if (entity == null) { + return null; + } + + return ProductDTO.builder() + .id(entity.getId()) + .name(entity.getName()) + .topic(entity.getTopic()) + .producerId(entity.getProducer() != null ? entity.getProducer().getId() : null) + .build(); + } + + /** + * Converts an OrganisationDataProviderDTO to an OrganisationDataProvider entity. + * + * @param dto the DTO to convert + * @return the converted entity + */ + @Override + public Product toEntity(ProductDTO dto) { + if (dto == null) { + return null; + } + + Product entity = new Product(); + entity.setId(dto.getId()); + entity.setName(dto.getName()); + entity.setTopic(dto.getTopic()); + + // Set the producer if producerId is provided + if (dto.getProducerId() != null) { + Producer producer = producerRepository.findById(dto.getProducerId()) + .orElse(null); + entity.setProducer(producer); + } + + return entity; + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/AuthenticationProcessingException.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/AuthenticationProcessingException.java new file mode 100644 index 0000000..7a5dd4a --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/AuthenticationProcessingException.java @@ -0,0 +1,43 @@ +package uk.gov.dbt.ndtp.ia.node.management.exception; + +import lombok.Getter; + +/** + * Base exception class for authentication processing errors. + * This exception is thrown when there's an error during the authentication process. + */ +@Getter +public class AuthenticationProcessingException extends RuntimeException { + + /** + * -- GETTER -- + * Gets the client ID associated with this exception. + * + * @return the client ID + */ + private final String clientId; + + /** + * Constructs a new AuthenticationProcessingException with the specified detail message. + * + * @param message the detail message + * @param clientId the client ID associated with the authentication + */ + public AuthenticationProcessingException(String message, String clientId) { + super(message + " [Client ID: " + clientId + "]"); + this.clientId = clientId; + } + + /** + * Constructs a new AuthenticationProcessingException with the specified detail message and cause. + * + * @param message the detail message + * @param cause the cause of the exception + * @param clientId the client ID associated with the authentication + */ + public AuthenticationProcessingException(String message, Throwable cause, String clientId) { + super(message + " [Client ID: " + clientId + "]", cause); + this.clientId = clientId; + } + +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/ErrorResponse.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/ErrorResponse.java new file mode 100644 index 0000000..e090f37 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/ErrorResponse.java @@ -0,0 +1,30 @@ +package uk.gov.dbt.ndtp.ia.node.management.exception; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents an error response to be sent to clients. + * Contains a status code, an error message, and a unique error ID without exposing stack traces. + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ErrorResponse { + + /** + * The HTTP status code + */ + private int status; + + /** + * A user-friendly error message + */ + private String message; + + /** + * A unique identifier for the error + */ + private String errorId; +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/JwtClaimParsingException.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/JwtClaimParsingException.java new file mode 100644 index 0000000..9a4403c --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/JwtClaimParsingException.java @@ -0,0 +1,30 @@ +package uk.gov.dbt.ndtp.ia.node.management.exception; + +/** + * Exception thrown when there's an error parsing JWT claims. + * This can happen when the JWT structure is unexpected or when claims + * don't contain the expected data. + */ +public class JwtClaimParsingException extends AuthenticationProcessingException { + + /** + * Constructs a new JwtClaimParsingException with the specified detail message. + * + * @param message the detail message + * @param clientId the client ID associated with the authentication + */ + public JwtClaimParsingException(String message, String clientId) { + super(message, clientId); + } + + /** + * Constructs a new JwtClaimParsingException with the specified detail message and cause. + * + * @param message the detail message + * @param cause the cause of the exception + * @param clientId the client ID associated with the authentication + */ + public JwtClaimParsingException(String message, Throwable cause, String clientId) { + super(message, cause, clientId); + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/ResourceAccessParsingException.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/ResourceAccessParsingException.java new file mode 100644 index 0000000..bb20257 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/ResourceAccessParsingException.java @@ -0,0 +1,29 @@ +package uk.gov.dbt.ndtp.ia.node.management.exception; + +/** + * Exception thrown when there's an error parsing the resource access information + * from the authentication token or introspection data. + */ +public class ResourceAccessParsingException extends AuthenticationProcessingException { + + /** + * Constructs a new ResourceAccessParsingException with the specified detail message. + * + * @param message the detail message + * @param clientId the client ID associated with the authentication + */ + public ResourceAccessParsingException(String message, String clientId) { + super(message, clientId); + } + + /** + * Constructs a new ResourceAccessParsingException with the specified detail message and cause. + * + * @param message the detail message + * @param cause the cause of the exception + * @param clientId the client ID associated with the authentication + */ + public ResourceAccessParsingException(String message, Throwable cause, String clientId) { + super(message, cause, clientId); + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/TokenIntrospectionException.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/TokenIntrospectionException.java new file mode 100644 index 0000000..175586b --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/TokenIntrospectionException.java @@ -0,0 +1,30 @@ +package uk.gov.dbt.ndtp.ia.node.management.exception; + +/** + * Exception thrown when there's an error during token introspection. + * This can happen when the introspection endpoint is unavailable, returns an error, + * or when the introspection data is invalid. + */ +public class TokenIntrospectionException extends AuthenticationProcessingException { + + /** + * Constructs a new TokenIntrospectionException with the specified detail message. + * + * @param message the detail message + * @param clientId the client ID associated with the authentication + */ + public TokenIntrospectionException(String message, String clientId) { + super(message, clientId); + } + + /** + * Constructs a new TokenIntrospectionException with the specified detail message and cause. + * + * @param message the detail message + * @param cause the cause of the exception + * @param clientId the client ID associated with the authentication + */ + public TokenIntrospectionException(String message, Throwable cause, String clientId) { + super(message, cause, clientId); + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/handlers/GlobalExceptionHandler.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/handlers/GlobalExceptionHandler.java new file mode 100644 index 0000000..a351695 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/exception/handlers/GlobalExceptionHandler.java @@ -0,0 +1,101 @@ +package uk.gov.dbt.ndtp.ia.node.management.exception.handlers; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import uk.gov.dbt.ndtp.ia.node.management.exception.AuthenticationProcessingException; +import uk.gov.dbt.ndtp.ia.node.management.exception.ErrorResponse; + +import java.util.UUID; + +/** + * Global exception handler for the application. + * Handles all exceptions thrown by controllers and provides appropriate responses + * without exposing stack traces to clients. + */ +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + /** + * Generates a unique error ID for tracking and correlation. + * + * @return a unique UUID string + */ + private String generateErrorId() { + return UUID.randomUUID().toString(); + } + + /** + * Handles AuthenticationProcessingException and its subclasses. + * + * @param ex the exception + * @param request the current request + * @return a ResponseEntity with an error message + */ + @ExceptionHandler(AuthenticationProcessingException.class) + public ResponseEntity handleAuthenticationProcessingException( + AuthenticationProcessingException ex, WebRequest request) { + + String errorId = generateErrorId(); + log.debug("Authentication processing exception occurred for client {}, error_id={}: ", + ex.getClientId(), errorId, ex); + + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.UNAUTHORIZED.value(), + "Authentication error: " + ex.getMessage(), + errorId + ); + + return new ResponseEntity<>(errorResponse, HttpStatus.UNAUTHORIZED); + } + + /** + * Handles RuntimeException. + * + * @param ex the exception + * @param request the current request + * @return a ResponseEntity with an error message + */ + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleRuntimeException( + RuntimeException ex, WebRequest request) { + + String errorId = generateErrorId(); + log.debug("Runtime exception occurred, error_id={}: ", errorId, ex); + + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + "An internal server error occurred", + errorId + ); + + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + + /** + * Handles all other exceptions. + * + * @param ex the exception + * @param request the current request + * @return a ResponseEntity with an error message + */ + @ExceptionHandler(Exception.class) + public ResponseEntity handleAllExceptions( + Exception ex, WebRequest request) { + + String errorId = generateErrorId(); + log.debug("Exception occurred, error_id={}: ", errorId, ex); + + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + "An unexpected error occurred", + errorId + ); + + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ConsumerConfigDTO.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ConsumerConfigDTO.java new file mode 100644 index 0000000..449a5ce --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ConsumerConfigDTO.java @@ -0,0 +1,13 @@ +package uk.gov.dbt.ndtp.ia.node.management.model.dto; + +import java.util.List; +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class ConsumerConfigDTO { + + private final String clientId; + private final List producers; +} diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ConsumerDTO.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ConsumerDTO.java new file mode 100644 index 0000000..eab5b78 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ConsumerDTO.java @@ -0,0 +1,25 @@ +package uk.gov.dbt.ndtp.ia.node.management.model.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * DTO for consumerId entity. + */ +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ConsumerDTO { + @JsonIgnore + private Long id; + private String name; + @JsonIgnore + private Long orgId; + private String idpClientId; +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ProducerConfigDTO.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ProducerConfigDTO.java new file mode 100644 index 0000000..447ebb5 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ProducerConfigDTO.java @@ -0,0 +1,13 @@ +package uk.gov.dbt.ndtp.ia.node.management.model.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Builder +@Getter +public class ProducerConfigDTO { + private String clientId; + private List producers; +} diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ProducerDTO.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ProducerDTO.java new file mode 100644 index 0000000..bcff794 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ProducerDTO.java @@ -0,0 +1,35 @@ +package uk.gov.dbt.ndtp.ia.node.management.model.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * DTO for OrganisationProducer entity. + */ +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ProducerDTO { + @JsonIgnore + private Long id; + private String name; + private String description; + @JsonIgnore + private Long orgId; + private Boolean active; + private String host; + private BigDecimal port; + private Boolean tls; + private String idpClientId; + private final List dataProviders = new ArrayList<>(); +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ProductConsumerDTO.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ProductConsumerDTO.java new file mode 100644 index 0000000..ba65767 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ProductConsumerDTO.java @@ -0,0 +1,25 @@ +package uk.gov.dbt.ndtp.ia.node.management.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; +import java.sql.Timestamp; + +/** + * DTO for ConsumerAllowedDataProvider entity. + */ +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ProductConsumerDTO { + private Long productId; + private Long consumerId; + private Timestamp grantedTs; + private BigDecimal validity; +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ProductDTO.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ProductDTO.java new file mode 100644 index 0000000..92e6efc --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/ProductDTO.java @@ -0,0 +1,28 @@ +package uk.gov.dbt.ndtp.ia.node.management.model.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +/** + * DTO for OrganisationDataProvider entity. + */ +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ProductDTO { + @JsonIgnore + private Long id; + private String name; + private String topic; + @JsonIgnore + private Long producerId; + private List consumers; +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/jwt/EnhancedPrincipal.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/jwt/EnhancedPrincipal.java new file mode 100644 index 0000000..0aa47e9 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/jwt/EnhancedPrincipal.java @@ -0,0 +1,42 @@ +package uk.gov.dbt.ndtp.ia.node.management.model.jwt; + +import lombok.Getter; + +import java.io.Serial; +import java.io.Serializable; + +/** + * Custom Principal object that includes clientId information from the JWT. + */ +@Getter +public class EnhancedPrincipal implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** + * -- GETTER -- + * Get the subject (user identifier) + * + */ + private final String subject; + /** + * -- GETTER -- + * Get the client ID + * + */ + private final String clientId; + + public EnhancedPrincipal(String subject, String clientId) { + this.subject = subject; + this.clientId = clientId; + } + + + @Override + public String toString() { + return "CustomPrincipal{" + + "subject='" + subject + '\'' + + ", clientId='" + clientId + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/jwt/JwtToken.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/jwt/JwtToken.java new file mode 100644 index 0000000..72c925f --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/jwt/JwtToken.java @@ -0,0 +1,46 @@ +package uk.gov.dbt.ndtp.ia.node.management.model.jwt; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Represents the structure of a JWT token. + * This class is used to deserialize the JSON response from the token introspection endpoint. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class JwtToken { + private Long exp; + private Long iat; + private String jti; + private String iss; + private List aud; + private String sub; + private String typ; + private String azp; + private List allowedOrigins; + private Map resourceAccess; + private String scope; + private String clientId; + private String username; + private String tokenType; + private Boolean active; + + /** + * Represents the resource access structure in the JWT token. + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ResourceAccess { + private List roles; + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/Consumer.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/Consumer.java new file mode 100644 index 0000000..ac7fba0 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/Consumer.java @@ -0,0 +1,32 @@ +package uk.gov.dbt.ndtp.ia.node.management.persistency.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@Entity +@Table(name = "consumer") +public class Consumer { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "name", nullable = false, length = 50) + private String name; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "org_id", nullable = false) + private Organisation org; + + @Column(name = "idp_client_id", nullable = false, length = 50) + private String idpClientId; + + @OneToMany(fetch = FetchType.LAZY) + @JoinColumn(name = "consumer_id",referencedColumnName = "id",insertable = false,updatable = false) + private List productConsumers; +} diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/Organisation.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/Organisation.java new file mode 100644 index 0000000..be5d7c8 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/Organisation.java @@ -0,0 +1,20 @@ +package uk.gov.dbt.ndtp.ia.node.management.persistency.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "organisation") +public class Organisation { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "name", nullable = false, length = 150) + private String name; + +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/Producer.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/Producer.java new file mode 100644 index 0000000..8289aa3 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/Producer.java @@ -0,0 +1,47 @@ +package uk.gov.dbt.ndtp.ia.node.management.persistency.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; +import java.util.List; + +@Getter +@Setter +@Entity +@Table(name = "producer") +public class Producer { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "name", nullable = false, length = 50) + private String name; + + @Column(name = "description", nullable = false, length = Integer.MAX_VALUE) + private String description; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "org_id", nullable = false) + private Organisation org; + + @Column(name = "active", nullable = false) + private Boolean active = false; + + @Column(name = "host", nullable = false, length = 500) + private String host; + + @Column(name = "port", nullable = false) + private BigDecimal port; + + @Column(name = "tls", nullable = false) + private Boolean tls = false; + + @Column(name = "idp_client_id", nullable = false, length = 50) + private String idpClientId; + + @OneToMany(mappedBy = "producer", fetch = FetchType.LAZY) + private List products; +} diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/Product.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/Product.java new file mode 100644 index 0000000..b08748f --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/Product.java @@ -0,0 +1,36 @@ +package uk.gov.dbt.ndtp.ia.node.management.persistency.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@Entity +@Table(name = "product") +public class Product { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "name", nullable = false, length = 50) + private String name; + + @Column(name = "topic", nullable = false, length = 150) + private String topic; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "producer_id", nullable = false) + private Producer producer; + + @OneToMany(fetch = FetchType.LAZY) + @JoinColumn( + name = "product_id", + referencedColumnName = "id", + insertable = false, + updatable = false) + private List productConsumer; +} diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/ProductConsumer.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/ProductConsumer.java new file mode 100644 index 0000000..f5d1ce1 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/ProductConsumer.java @@ -0,0 +1,28 @@ +package uk.gov.dbt.ndtp.ia.node.management.persistency.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; +import java.sql.Timestamp; + +@Getter +@Setter +@Entity +@Table(name = "product_consumer") +public class ProductConsumer { + @EmbeddedId + private ProductConsumerId id; + + @Column(name = "granted_ts", nullable = false) + private Timestamp grantedTs; + + @Column(name = "validity", nullable = false) + private BigDecimal validity; + + +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/ProductConsumerId.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/ProductConsumerId.java new file mode 100644 index 0000000..2e8004f --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/ProductConsumerId.java @@ -0,0 +1,39 @@ +package uk.gov.dbt.ndtp.ia.node.management.persistency.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.Hibernate; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Objects; + +@Getter +@Setter +@Embeddable +public class ProductConsumerId implements Serializable { + @Serial + private static final long serialVersionUID = -1247742635043749804L; + @Column(name = "product_id", nullable = false) + private Long productId; + + @Column(name = "consumer_id", nullable = false) + private Long consumerId; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; + ProductConsumerId entity = (ProductConsumerId) o; + return Objects.equals(this.consumerId, entity.consumerId) && + Objects.equals(this.productId, entity.productId); + } + + @Override + public int hashCode() { + return Objects.hash(consumerId, productId); + } + +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ConsumerProviderRepository.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ConsumerProviderRepository.java new file mode 100644 index 0000000..29cf146 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ConsumerProviderRepository.java @@ -0,0 +1,20 @@ +package uk.gov.dbt.ndtp.ia.node.management.persistency.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumerId; + +import java.util.List; + +@Repository +public interface ConsumerProviderRepository extends JpaRepository { + + @Query("Select dp from ProductConsumer dp where dp.id.consumerId=:consumerId") + List findByConsumerId(@Param("consumerId") Long consumerId); + + @Query("Select dp from ProductConsumer dp where dp.id.productId=:productId") + List findByProductId(Long productId); +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ConsumerRepository.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ConsumerRepository.java new file mode 100644 index 0000000..838f1d5 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ConsumerRepository.java @@ -0,0 +1,18 @@ +package uk.gov.dbt.ndtp.ia.node.management.persistency.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Consumer; + +import java.util.List; + +@Repository +public interface ConsumerRepository extends JpaRepository { + + List findByIdpClientId(String clientId); + + @Query("SELECT c FROM Consumer c JOIN c.productConsumers cp WHERE cp.id.productId IN :providers") + List findConsumersByProviderIds(List providers); + +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/OrganisationRepository.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/OrganisationRepository.java new file mode 100644 index 0000000..299e60b --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/OrganisationRepository.java @@ -0,0 +1,9 @@ +package uk.gov.dbt.ndtp.ia.node.management.persistency.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Organisation; + +@Repository +public interface OrganisationRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ProducerRepository.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ProducerRepository.java new file mode 100644 index 0000000..a6210ee --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ProducerRepository.java @@ -0,0 +1,18 @@ +package uk.gov.dbt.ndtp.ia.node.management.persistency.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Producer; + +import java.util.List; + +@Repository +public interface ProducerRepository extends JpaRepository { + + @Query("SELECT o FROM Producer o JOIN FETCH o.products WHERE o.id IN :ids") + List findByIds(List ids); + + @Query("SELECT o FROM Producer o JOIN FETCH o.products WHERE o.idpClientId IN :idpClientId") + List findByIdpClientId (String idpClientId); +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ProductRepository.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ProductRepository.java new file mode 100644 index 0000000..14a5a09 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ProductRepository.java @@ -0,0 +1,19 @@ +package uk.gov.dbt.ndtp.ia.node.management.persistency.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Product; + +import java.util.List; + +@Repository +public interface ProductRepository extends JpaRepository { + + @Query("SELECT o FROM Product o WHERE o.id IN :ids") + List findByIds(List ids); + + @Query("SELECT o FROM Product o WHERE o.producer.id IN :producers") + List findByProducerIds(List producers); + +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/ConsumerService.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/ConsumerService.java new file mode 100644 index 0000000..a6aae47 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/ConsumerService.java @@ -0,0 +1,40 @@ +package uk.gov.dbt.ndtp.ia.node.management.service.data; + +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ConsumerDTO; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Service interface for managing ConsumerId entities. + */ +public interface ConsumerService { + /** + * Find a ConsumerId by its ID. + * + * @param id the IDP client ID to search for + * @return a list of ConsumerIdDTO objects matching the ID + */ + Optional findById(Long id); + + + + /** + * Find an ConsumerId by its IDP client ID. + * + * @param idpClientId the IDP client ID to search for + * @return a list of ConsumerIdDTO objects matching the IDP client ID + */ + List findByIdpClientId(String idpClientId); + + + /** + * Retrieves a map of consumers identified by their client_id + * + * @param providers a list of provider IDs for which associated consumers need to be retrieved + * @return a map where the keys are provider IDs and the values are lists of ConsumerDTO objects + * representing the consumers associated with each provider + */ + Map> getConsumersOfProviders(List providers); +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/OrganisationService.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/OrganisationService.java new file mode 100644 index 0000000..8bdf87c --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/OrganisationService.java @@ -0,0 +1,10 @@ +package uk.gov.dbt.ndtp.ia.node.management.service.data; + + +/** + * Service interface for managing Organisation entities. + */ +public interface OrganisationService { + + +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/ProducerService.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/ProducerService.java new file mode 100644 index 0000000..810cc3d --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/ProducerService.java @@ -0,0 +1,23 @@ +package uk.gov.dbt.ndtp.ia.node.management.service.data; + +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProducerDTO; + +import java.util.List; + +/** + * Service interface for managing OrganisationProducer entities. + */ +public interface ProducerService { + + /** + * Retrieves a map of organisation IDs to lists of producer DTOs associated with those organisations. + * + * @param producerIds the list of producer IDs to retrieve + * @return a map where keys are organisation IDs and values are lists of producer DTOs associated with each organisation + */ + List getProducersByIds(List producerIds); + + + List getProducersByClientId(String clientId); + +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/ProductConsumerService.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/ProductConsumerService.java new file mode 100644 index 0000000..79b8341 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/ProductConsumerService.java @@ -0,0 +1,22 @@ +package uk.gov.dbt.ndtp.ia.node.management.service.data; + +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProductConsumerDTO; + +import java.util.List; + +/** + * Service interface for managing ConsumerAllowedDataProvider entities. + */ +public interface ProductConsumerService { + + /** + * Find all ConsumerAllowedDataProvider entities by consumer ID. + * + * @param consumerId the consumer ID + * @return a list of ConsumerAllowedDataProviderDTO objects + */ + List findByConsumerId(Long consumerId); + + List findByDataProviderId(Long providerId); + +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/ProductService.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/ProductService.java new file mode 100644 index 0000000..a0ef2ee --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/ProductService.java @@ -0,0 +1,28 @@ +package uk.gov.dbt.ndtp.ia.node.management.service.data; + +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProductDTO; + +import java.util.List; + +/** + * Service interface for managing OrganisationDataProvider entities. + */ +public interface ProductService { + + /** + * Retrieves a list of OrganisationDataProviderDTO objects by their IDs. + * + * @param ids the list of IDs to search for + * @return a list of OrganisationDataProviderDTO objects + */ + List getProductsByIds(List ids); + + + /** + * Retrieves a list of DataProviderDTO objects associated with the specified producer IDs. + * + * @param ProducerIds the list of producer IDs for which data providers need to be retrieved + * @return a list of DataProviderDTO objects corresponding to the given producer IDs + */ + List getProductsByProducerIds(List ProducerIds); +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ConsumerServiceImpl.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ConsumerServiceImpl.java new file mode 100644 index 0000000..420734a --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ConsumerServiceImpl.java @@ -0,0 +1,60 @@ +package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; + +import org.springframework.stereotype.Service; +import uk.gov.dbt.ndtp.ia.node.management.converter.impl.ConsumerConverter; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ConsumerDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Consumer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.ConsumerRepository; +import uk.gov.dbt.ndtp.ia.node.management.service.data.ConsumerService; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Implementation of the consumerIdService interface. + */ +@Service +public class ConsumerServiceImpl implements ConsumerService { + + private final ConsumerRepository consumerRepository; + private final ConsumerConverter consumerIdConverter; + + /** + * Constructor-based dependency injection. + * + * @param consumerRepository the organisation consumer repository + * @param consumerIdConverter the converter for entity-to-DTO conversion + */ + public ConsumerServiceImpl(ConsumerRepository consumerRepository, + ConsumerConverter consumerIdConverter) { + this.consumerRepository = consumerRepository; + this.consumerIdConverter = consumerIdConverter; + } + + @Override + public Optional findById(Long id) { + Optional consumer = consumerRepository.findById(id); + return consumer.map(consumerIdConverter::toDto); + } + + /** + * {@inheritDoc} + */ + @Override + public List findByIdpClientId(String idpClientId) { + List consumers = consumerRepository.findByIdpClientId(idpClientId); + return consumerIdConverter.toDtoList(consumers); + } + +@Override +public Map> getConsumersOfProviders(List providers) { + List consumers = consumerRepository.findConsumersByProviderIds(providers); + return consumers.stream() + .map(consumerIdConverter::toDto) + .collect( + Collectors.groupingBy( + ConsumerDTO::getIdpClientId, Collectors.mapping(dto -> dto, Collectors.toList()))); + } +} diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/OrganisationServiceImpl.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/OrganisationServiceImpl.java new file mode 100644 index 0000000..9c1de38 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/OrganisationServiceImpl.java @@ -0,0 +1,25 @@ +package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; + +import org.springframework.stereotype.Service; +import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.OrganisationRepository; +import uk.gov.dbt.ndtp.ia.node.management.service.data.OrganisationService; + +/** + * Implementation of the OrganisationService interface. + */ +@Service +public class OrganisationServiceImpl implements OrganisationService { + + private final OrganisationRepository organisationRepository; + + /** + * Constructor-based dependency injection. + * + * @param organisationRepository the organisation repository + */ + public OrganisationServiceImpl(OrganisationRepository organisationRepository) { + this.organisationRepository = organisationRepository; + } + + +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProducerServiceImpl.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProducerServiceImpl.java new file mode 100644 index 0000000..d5b41bf --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProducerServiceImpl.java @@ -0,0 +1,48 @@ +package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; + +import org.springframework.stereotype.Service; +import uk.gov.dbt.ndtp.ia.node.management.converter.impl.OrganisationProducerConverter; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProducerDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Producer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.ProducerRepository; +import uk.gov.dbt.ndtp.ia.node.management.service.data.ProducerService; + +import java.util.List; + +/** + * Implementation of the OrganisationProducerService interface. + */ +@Service +public class ProducerServiceImpl implements ProducerService { + + private final ProducerRepository producerRepository; + private final OrganisationProducerConverter organisationProducerConverter; + + /** + * Constructor-based dependency injection. + * + * @param producerRepository the organisation producer repository + * @param organisationProducerConverter the converter for entity-to-DTO conversion + */ + public ProducerServiceImpl(ProducerRepository producerRepository, + OrganisationProducerConverter organisationProducerConverter) { + this.producerRepository = producerRepository; + this.organisationProducerConverter = organisationProducerConverter; + + } + + /** {@inheritDoc} */ + @Override + public List getProducersByIds(List producerIds) { + List producers = producerRepository.findByIds(producerIds); + + // Convert entities to DTOs using the converter + return organisationProducerConverter.toDtoList(producers); + } + + @Override + public List getProducersByClientId(String clientId) { + List producers = producerRepository.findByIdpClientId(clientId); + return organisationProducerConverter.toDtoList(producers); + } +} diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProductConsumerServiceImpl.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProductConsumerServiceImpl.java new file mode 100644 index 0000000..aec29da --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProductConsumerServiceImpl.java @@ -0,0 +1,49 @@ +package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; + +import org.springframework.stereotype.Service; +import uk.gov.dbt.ndtp.ia.node.management.converter.impl.ProductConsumerConverter; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProductConsumerDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.ConsumerProviderRepository; +import uk.gov.dbt.ndtp.ia.node.management.service.data.ProductConsumerService; + +import java.util.List; + +/** + * Implementation of the ConsumerAllowedDataProviderService interface. + */ +@Service +public class ProductConsumerServiceImpl implements ProductConsumerService { + + private final ConsumerProviderRepository consumerProviderRepository; + private final ProductConsumerConverter consumerProviderConverter; + + /** + * Constructor-based dependency injection. + * + * @param consumerProviderRepository the consumer allowed data provider repository + * @param productConsumerConverter the converter for entity-to-DTO conversion + */ + public ProductConsumerServiceImpl( + ConsumerProviderRepository consumerProviderRepository, + ProductConsumerConverter productConsumerConverter) { + this.consumerProviderRepository = consumerProviderRepository; + this.consumerProviderConverter = productConsumerConverter; + } + + /** + * {@inheritDoc} + */ + @Override + public List findByConsumerId(Long consumerId) { + List entities = consumerProviderRepository.findByConsumerId(consumerId); + return consumerProviderConverter.toDtoList(entities); + } + + @Override + public List findByDataProviderId(Long providerId) { + List entities = consumerProviderRepository.findByProductId(providerId); + return consumerProviderConverter.toDtoList(entities); + } + +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProductServiceImpl.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProductServiceImpl.java new file mode 100644 index 0000000..5c7033c --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProductServiceImpl.java @@ -0,0 +1,61 @@ +package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; + +import org.springframework.stereotype.Service; +import uk.gov.dbt.ndtp.ia.node.management.converter.impl.ProductConverter; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProductDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Product; +import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.ProductRepository; +import uk.gov.dbt.ndtp.ia.node.management.service.data.ProductService; + +import java.util.List; +import java.util.Optional; + +/** + * Implementation of the OrganisationDataProviderService interface. + */ +@Service +public class ProductServiceImpl implements ProductService { + + private final ProductRepository productRepository; + private final ProductConverter productConverter; + + /** + * Constructor-based dependency injection. + * + * @param productRepository the organisation data provider repository + * @param productConverter the converter for entity-to-DTO conversion + */ + public ProductServiceImpl( + ProductRepository productRepository, + ProductConverter productConverter) { + this.productRepository = productRepository; + this.productConverter = productConverter; + } + + /** + * {@inheritDoc} + */ + @Override + public List getProductsByIds(List ids) { + if (ids == null || ids.isEmpty()) { + return List.of(); + } + List dataProviders = + productRepository.findByIds(ids); + return Optional.ofNullable(dataProviders) + .map(productConverter::toDtoList) + .orElse(List.of()); + } + + /** + * {@inheritDoc} + */ + @Override + public List getProductsByProducerIds(List producerIds) { + List dataProviders = productRepository.findByProducerIds(producerIds); + return Optional.ofNullable(dataProviders) + .map(productConverter::toDtoList) + .orElse(List.of()); + } + +} \ No newline at end of file diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/providers/configuration/ConfigurationProvider.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/providers/configuration/ConfigurationProvider.java new file mode 100644 index 0000000..ae26fe2 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/providers/configuration/ConfigurationProvider.java @@ -0,0 +1,42 @@ +package uk.gov.dbt.ndtp.ia.node.management.service.providers.configuration; + +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ConsumerConfigDTO; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProducerConfigDTO; + +import java.util.Optional; + +/** + * Interface for retrieving organization configuration information for both consumers and producers. + *

+ * This provider interface defines methods to access configuration settings for organizations + * based on their client identifiers. It serves as a central point for retrieving configuration + * data that may be stored in various backend systems or repositories. + *

+ * + * @since 1.0 + */ +public interface ConfigurationProvider { + + /** + * Retrieves the configuration for a consumer organization identified by the given client ID. + * + * @param clientId The unique identifier for the consumer organization. Must not be null or blank. + * @param consumerId An optional identifier for the consumer. This can provide further specificity to the request. + * @return The configuration settings for the specified consumer organization. + * @throws IllegalArgumentException if the clientId is null or empty. + * @throws RuntimeException if the configuration cannot be retrieved due to system errors. + */ + ConsumerConfigDTO getConsumerConfigByClientId(String clientId, Optional consumerId); + + /** + * Retrieves the configuration for a producer organization identified by the given client ID. + * + * @param clientId The unique identifier for the producer organization. Must not be null or blank. + * @param producerId An optional identifier for the producer. This can provide further specificity to the request. + * @return The configuration settings for the specified producer organization. + * @throws IllegalArgumentException if the clientId is null or empty. + * @throws RuntimeException if the configuration cannot be retrieved due to system errors. + */ + ProducerConfigDTO getProducerConfigByClientId(String clientId, Optional producerId); + +} diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/providers/configuration/ConfigurationProviderImpl.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/providers/configuration/ConfigurationProviderImpl.java new file mode 100644 index 0000000..1ca41ca --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/service/providers/configuration/ConfigurationProviderImpl.java @@ -0,0 +1,229 @@ +package uk.gov.dbt.ndtp.ia.node.management.service.providers.configuration; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.springframework.stereotype.Service; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.*; +import uk.gov.dbt.ndtp.ia.node.management.service.data.ProductConsumerService; +import uk.gov.dbt.ndtp.ia.node.management.service.data.ConsumerService; +import uk.gov.dbt.ndtp.ia.node.management.service.data.ProductService; +import uk.gov.dbt.ndtp.ia.node.management.service.data.ProducerService; + +@Service +public class ConfigurationProviderImpl implements ConfigurationProvider { + + private final ConsumerService consumerService; + + private final ProductConsumerService consumerAllowedDataProvidersService; + + private final ProductService dataProviderService; + + private final ProducerService producerService; + + public ConfigurationProviderImpl( + ConsumerService consumerService, + ProductConsumerService consumerAllowedDataProviders, + ProductService dataProviderService, + ProducerService producerService) { + + this.consumerService = consumerService; + this.consumerAllowedDataProvidersService = consumerAllowedDataProviders; + this.dataProviderService = dataProviderService; + this.producerService = producerService; + } + + + + @Override + public ConsumerConfigDTO getConsumerConfigByClientId(String clientId, Optional consumerId) { + List consumers = getFilteredConsumers(clientId, consumerId); + List consumerAllowedDataProviders = getValidDataProviders(consumers); + List dataProviders = getDataProvidersForConsumers(consumerAllowedDataProviders); + List producers = getActiveProducersForDataProviders(dataProviders); + + return ConsumerConfigDTO.builder().clientId(clientId).producers(producers).build(); + } + + @Override + public ProducerConfigDTO getProducerConfigByClientId(String clientId, Optional producerId) { + List producers = getFilteredActiveProducers(clientId, producerId); + List dataProviderIds = collectDataProviderIds(producers); + + // Get allowed consumers (not directly used but might be needed for side effects) + consumerService.getConsumersOfProviders(dataProviderIds); + + // Process consumers for each provider + processConsumersForProducers(producers); + + return ProducerConfigDTO.builder().clientId(clientId).producers(producers).build(); + } + + /** + * Filters consumers by client ID and optional consumer ID. + * + * @param clientId the client ID to filter by + * @param consumerId optional consumer ID for additional filtering + * @return filtered list of consumers + */ + private List getFilteredConsumers(String clientId, Optional consumerId) { + List consumers = consumerService.findByIdpClientId(clientId); + + if (consumerId.isPresent()) { + consumers = consumers.stream() + .filter(consumer -> consumer.getId().equals(consumerId.get())) + .toList(); + } + + return consumers; + } + + /** + * Retrieves data providers for the given consumer-product relationships. + * + * @param consumerAllowedDataProviders list of consumer-product relationships + * @return list of data providers + */ + private List getDataProvidersForConsumers(List consumerAllowedDataProviders) { + List dataProviderIds = consumerAllowedDataProviders.stream() + .map(ProductConsumerDTO::getProductId) + .toList(); + + return dataProviderService.getProductsByIds(dataProviderIds); + } + + /** + * Retrieves and filters active producers for the given data providers. + * + * @param dataProviders list of data providers + * @return list of active producers + */ + private List getActiveProducersForDataProviders(List dataProviders) { + List producerIds = dataProviders.stream() + .map(ProductDTO::getProducerId) + .toList(); + + return producerService.getProducersByIds(producerIds).stream() + .filter(ProducerDTO::getActive) + .toList(); + } + + + + private List getValidDataProviders(List consumers) { + return consumers.stream() + .map(consumer -> consumerAllowedDataProvidersService.findByConsumerId(consumer.getId())) + .flatMap(List::stream) + .filter(this::isValidProvider) + .toList(); + } + + /** + * Filters active producers by client ID and optional producer ID. + * + * @param clientId the client ID to filter by + * @param producerId optional producer ID for additional filtering + * @return filtered list of active producers + */ + private List getFilteredActiveProducers(String clientId, Optional producerId) { + List producers = producerService.getProducersByClientId(clientId).stream() + .filter(ProducerDTO::getActive) + .toList(); + + if (producerId.isPresent()) { + producers = producers.stream() + .filter(producer -> producerId.get().equals(producer.getId())) + .toList(); + } + + return producers; + } + + /** + * Collects all data provider IDs from the given producers. + * + * @param producers list of producers + * @return list of data provider IDs + */ + private List collectDataProviderIds(List producers) { + List dataProviderIds = new ArrayList<>(); + + for (ProducerDTO producer : producers) { + List ids = producer.getDataProviders().stream() + .map(ProductDTO::getId) + .toList(); + dataProviderIds.addAll(ids); + } + + return dataProviderIds; + } + + /** + * Processes consumers for each provider in the given producers. + * + * @param producers list of producers to process + */ + private void processConsumersForProducers(List producers) { + for (ProducerDTO producer : producers) { + for (ProductDTO provider : producer.getDataProviders()) { + processConsumersForProvider(provider); + } + } + } + + /** + * Processes consumers for a specific provider. + * + * @param provider the provider to process consumers for + */ + private void processConsumersForProvider(ProductDTO provider) { + // Initialize consumers list if null + if (provider.getConsumers() == null) { + provider.setConsumers(new ArrayList<>()); + } + + // Get consumer providers for this data provider + List consumerProviders = + consumerAllowedDataProvidersService.findByDataProviderId(provider.getId()); + + // Filter valid providers and add their consumers + addValidConsumersToProvider(consumerProviders, provider); + } + + /** + * Adds valid consumers to the given provider. + * + * @param consumerProviders list of consumer-provider relationships + * @param provider the provider to add consumers to + */ + private void addValidConsumersToProvider(List consumerProviders, ProductDTO provider) { + consumerProviders.stream() + .filter(this::isValidProvider) + .forEach(consumerProvider -> { + Optional consumer = consumerService.findById(consumerProvider.getConsumerId()); + consumer.ifPresent(provider.getConsumers()::add); + }); + } + + + + + private boolean isValidProvider(ProductConsumerDTO provider) { + + if (provider.getValidity() == null || provider.getValidity().equals(BigDecimal.ZERO)) + return true; + + return isValidGrantedTs(provider.getGrantedTs(), provider.getValidity()); + } + + private static boolean isValidGrantedTs(Timestamp grantedTs, BigDecimal validity) { + return grantedTs != null + && grantedTs + .toInstant() + .plus(java.time.Duration.ofDays(validity.longValue())) + .isAfter(Instant.now()); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..7ad2d89 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,60 @@ +spring: + application: + name: management-node + security: + oauth2: + resourceserver: + jwt: + issuer-uri: https://localhost:8443/realms/management-node + jwk-set-uri: https://localhost:8443/realms/management-node/protocol/openid-connect/certs + audiences: management-node + authorities-claim-name: resource_access + opaquetoken: + introspection-uri: https://localhost:8443/realms/management-node/protocol/openid-connect/token/introspect + client-secret: + client-id: + flyway: + create-schemas: on + default-schema: mn + locations: classpath:db/migration,classpath:db/samples + enabled: true + baseline-on-migrate: true + datasource: + url: jdbc:postgresql://localhost:5433/postgres + username: keycloak_db_user + password: + jpa: + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + show_sql: true + default_schema: mn + +# Server configuration +server: + port: 8090 + ssl: + key-alias: localhost + key-store: keystore.jks + key-store-type: JKS + key-store-password: + trust-store: truststore.jks + trust-store-password: + trust-store-type: JKS +# Actuator Configuration +management: + endpoints: + web: + exposure: + include: health,info,metrics + endpoint: + health: + show-details: when_authorized +# Logging Configuration +logging: + level: + org.springframework.security: DEBUG + uk.gov.dbt.ndtp.ia.node.management: DEBUG + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{clientId}] - %msg%n" + file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{clientId}] - %msg%n" \ No newline at end of file diff --git a/src/main/resources/db/migration/V20250728142253__intial_database_tables.sql b/src/main/resources/db/migration/V20250728142253__intial_database_tables.sql new file mode 100644 index 0000000..96dfea3 --- /dev/null +++ b/src/main/resources/db/migration/V20250728142253__intial_database_tables.sql @@ -0,0 +1,71 @@ +create table organisation +( + id bigserial + constraint pk_organisation + primary key, + name varchar(150) not null +); + + + +create table producer +( + id bigserial + constraint pk_producer + primary key, + name varchar(50) not null, + description text not null, + org_id bigint not null + constraint fk___org_id + references organisation, + active boolean not null, + host varchar(500) not null, + port numeric not null, + tls boolean not null, + idp_client_id varchar(50) not null +); + + + +create table consumer +( + id bigserial + constraint pk_consumer + primary key, + name varchar(50) not null, + org_id bigint not null + constraint fk__org_id + references organisation, + idp_client_id varchar(50) not null +); + + + +create table product +( + id bigserial not null + constraint pk_3 + primary key, + name varchar(50) not null, + topic varchar(150) not null, + producer_id bigint not null + constraint fk_2 + references producer +); + + + +create table product_consumer +( + product_id bigint not null + constraint fk_organisation_data_provider__organisation_data_provider_id + references product, + consumer_id bigint not null + constraint fk_organisation_consumer__organisation_consumer_id + references consumer, + granted_ts timestamp not null, + validity numeric not null, + constraint pk_consumer_allowed_data_provider + primary key (product_id, consumer_id) +); + diff --git a/src/main/resources/db/samples/V20250728152300__sample_data.sql b/src/main/resources/db/samples/V20250728152300__sample_data.sql new file mode 100644 index 0000000..5a0de90 --- /dev/null +++ b/src/main/resources/db/samples/V20250728152300__sample_data.sql @@ -0,0 +1,61 @@ +-- Sample data for organisation table +INSERT INTO organisation (name ) VALUES ('Environment Agency (ENV)'); +INSERT INTO organisation (name ) VALUES ('Bristol City Council (BCC)'); +INSERT INTO organisation (name ) VALUES ('Homes England (HEG)'); + + +-- Sample data for producer table +INSERT INTO producer (name, description, org_id, active, host, port, tls, idp_client_id) +VALUES ('ENV-PRODUCER-1', 'ENV Producer 1', (select id from organisation where name like '%ENV%'), true, 'https://env.gov.uk', 443, true, 'FEDERATOR_ENV'); + +INSERT INTO producer (name, description, org_id, active, host, port, tls, idp_client_id) +VALUES ('HEG-PRODUCER-1', 'HEG Producer 1', (select id from organisation where name like '%HEG%'), true, 'https://heg.gov.uk', 443, true, 'FEDERATOR_HEG'); + + +INSERT INTO producer (name, description, org_id, active, host, port, tls, idp_client_id) +VALUES ('BCC-PRODUCER-1', 'BCC Producer 1', (select id from organisation where name like '%BCC%'), true, 'https://heg.gov.uk', 443, true, 'FEDERATOR_BCC'); + + + +-- Sample data for consumer table + + +INSERT INTO consumer (name, org_id, idp_client_id) +VALUES ('ENV-CONSUMER-1', (select id from organisation where name like '%ENV%'), 'FEDERATOR_ENV'); + + +INSERT INTO consumer (name, org_id, idp_client_id) +VALUES ('BCC-CONSUMER-1', (select id from organisation where name like '%BCC%'), 'FEDERATOR_BCC'); + + +INSERT INTO consumer (name, org_id, idp_client_id) +VALUES ('HEG-CONSUMER-1', (select id from organisation where name like '%HEG%'), 'FEDERATOR_HEG'); + +-- Sample data for data_provider table +INSERT INTO product (name, topic, producer_id) +VALUES ('BrownfieldLandAvailability', 'topic.BrownfieldLandAvailability', (select id from producer where name like '%HEG%'));; + +INSERT INTO product (name, topic, producer_id) +VALUES ('PendingPlanningApplications', 'topic.PendingPlanningApplications', (select id from producer where name like '%BCC%'));; + +INSERT INTO product (name, topic, producer_id) +VALUES ('FloodRiskMapZones', 'topic.FloodRiskMapZones', (select id from producer where name like '%ENV%'));; + + + + + +-- Sample data for consumer_provider table +INSERT INTO product_consumer (product_id, consumer_id, granted_ts, validity) +VALUES ((select id from product where name ='FloodRiskMapZones'), (select id from consumer where name like '%BCC%' ) , '2025-07-01 00:00:00', 365); + + +INSERT INTO product_consumer (product_id, consumer_id, granted_ts, validity) +VALUES ((select id from product where name ='PendingPlanningApplications'), (select id from consumer where name like '%HEG%' ) , '2025-07-01 00:00:00', 365); + + + +INSERT INTO product_consumer (product_id, consumer_id, granted_ts, validity) +VALUES ((select id from product where name ='BrownfieldLandAvailability'), (select id from consumer where name like '%ENV%' ) , '2025-07-01 00:00:00', 365); + + diff --git a/src/test/java/uk/gov/dbt/ndtp/ia/node/management/ManagementNodeApplicationTests.java b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/ManagementNodeApplicationTests.java new file mode 100644 index 0000000..85e8221 --- /dev/null +++ b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/ManagementNodeApplicationTests.java @@ -0,0 +1,13 @@ +package uk.gov.dbt.ndtp.ia.node.management; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ManagementNodeApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/uk/gov/dbt/ndtp/ia/node/management/config/KeycloakJwtAuthenticationConverterExceptionTest.java b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/config/KeycloakJwtAuthenticationConverterExceptionTest.java new file mode 100644 index 0000000..5073d33 --- /dev/null +++ b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/config/KeycloakJwtAuthenticationConverterExceptionTest.java @@ -0,0 +1,196 @@ +package uk.gov.dbt.ndtp.ia.node.management.config; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import uk.gov.dbt.ndtp.ia.node.management.model.jwt.EnhancedPrincipal; + +import java.time.Instant; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +/** + * Tests specifically for exception handling in KeycloakJwtAuthenticationConverter. + */ +@ExtendWith(MockitoExtension.class) +class KeycloakJwtAuthenticationConverterExceptionTest { + + @Mock + private RestTemplate restTemplate; + + @InjectMocks + private KeycloakJwtAuthenticationConverter converter; + + private Jwt mockJwt; + + @BeforeEach + void setUp() { + // Set up configuration properties + ReflectionTestUtils.setField(converter, "introspectionUri", "http://localhost:8080/realms/management-node/protocol/openid-connect/token/introspect"); + ReflectionTestUtils.setField(converter, "clientId", "management-node"); + ReflectionTestUtils.setField(converter, "clientSecret", "0T5S4wNAPaaOUzFVFQyenorSEC6zxcb0"); + + // Create a mock JWT with the sample token data + Map headers = new HashMap<>(); + headers.put("alg", "RS256"); + headers.put("typ", "JWT"); + + Map claims = new HashMap<>(); + claims.put("exp", 1753576065); + claims.put("iat", 1753575765); + claims.put("jti", "trrtcc:713a03f4-55fb-4198-e8ea-a1be37d5f52f"); + claims.put("iss", "http://localhost:8080/realms/management-node"); + claims.put("sub", "86a41a8a-ab2e-465e-8b48-a09d3275f842"); + claims.put("typ", "Bearer"); + claims.put("azp", "management-node"); + claims.put("client_id", "management-node"); + + // Set up the aud claim as a list + List audiences = Arrays.asList("F1", "F2"); + claims.put("aud", audiences); + + // Set up resource_access claim with nested roles + Map resourceAccess = new HashMap<>(); + Map f1Resource = new HashMap<>(); + List f1Roles = Arrays.asList("TOPIC_2", "TOPIC_1"); + f1Resource.put("roles", f1Roles); + resourceAccess.put("F1", f1Resource); + claims.put("resource_access", resourceAccess); + + // Create the JWT with the headers and claims + mockJwt = new Jwt( + "token-value", + Instant.ofEpochSecond(1753575765), + Instant.ofEpochSecond(1753576065), + headers, + claims + ); + + // Inject mock RestTemplate + ReflectionTestUtils.setField(converter, "restTemplate", restTemplate); + } + + @Test + void convert_withRestClientException_shouldFallbackToJwtParsing() { + // Arrange + // Configure RestTemplate to throw a RestClientException + when(restTemplate.postForEntity( + anyString(), + any(HttpEntity.class), + Mockito.>any() + )).thenThrow(new RestClientException("Connection refused")); + + // Act + AbstractAuthenticationToken token = converter.convert(mockJwt); + + // Assert + assertNotNull(token); + assertTrue(token instanceof CustomJwtAuthenticationToken); + + // Verify the token has the correct principal + EnhancedPrincipal principal = ((CustomJwtAuthenticationToken) token).getPrincipal(); + assertEquals("86a41a8a-ab2e-465e-8b48-a09d3275f842", principal.getSubject()); + assertEquals("management-node", principal.getClientId()); + } + + @Test + void convert_withMalformedIntrospectionResponse_shouldFallbackToJwtParsing() { + // Arrange + // Create a malformed introspection response that will cause a ResourceAccessParsingException + Map malformedResponse = new HashMap<>(); + malformedResponse.put("active", true); + malformedResponse.put("sub", "86a41a8a-ab2e-465e-8b48-a09d3275f842"); + malformedResponse.put("client_id", "management-node"); + + // Add malformed resource_access (not a map but a string) + malformedResponse.put("resource_access", "not-a-map"); + + // Configure RestTemplate to return the malformed response + ResponseEntity responseEntity = new ResponseEntity<>(malformedResponse, HttpStatus.OK); + when(restTemplate.postForEntity( + anyString(), + any(HttpEntity.class), + Mockito.>any() + )).thenReturn(responseEntity); + + // Act + AbstractAuthenticationToken token = converter.convert(mockJwt); + + // Assert + assertNotNull(token); + assertTrue(token instanceof CustomJwtAuthenticationToken); + + // Verify the token has the correct principal + EnhancedPrincipal principal = ((CustomJwtAuthenticationToken) token).getPrincipal(); + assertEquals("86a41a8a-ab2e-465e-8b48-a09d3275f842", principal.getSubject()); + assertEquals("management-node", principal.getClientId()); + } + + @Test + void convert_withInactiveToken_shouldFallbackToJwtParsing() { + // Arrange + // Create an introspection response with inactive token + Map inactiveTokenResponse = new HashMap<>(); + inactiveTokenResponse.put("active", false); + + // Configure RestTemplate to return the inactive token response + ResponseEntity responseEntity = new ResponseEntity<>(inactiveTokenResponse, HttpStatus.OK); + when(restTemplate.postForEntity( + anyString(), + any(HttpEntity.class), + Mockito.>any() + )).thenReturn(responseEntity); + + // Act + AbstractAuthenticationToken token = converter.convert(mockJwt); + + // Assert + assertNotNull(token); + assertTrue(token instanceof CustomJwtAuthenticationToken); + + // Verify the token has the correct principal + EnhancedPrincipal principal = ((CustomJwtAuthenticationToken) token).getPrincipal(); + assertEquals("86a41a8a-ab2e-465e-8b48-a09d3275f842", principal.getSubject()); + assertEquals("management-node", principal.getClientId()); + } + + @Test + void convert_withNullIntrospectionResponse_shouldFallbackToJwtParsing() { + // Arrange + // Configure RestTemplate to return null response body + ResponseEntity responseEntity = new ResponseEntity<>(null, HttpStatus.OK); + when(restTemplate.postForEntity( + anyString(), + any(HttpEntity.class), + Mockito.>any() + )).thenReturn(responseEntity); + + // Act + AbstractAuthenticationToken token = converter.convert(mockJwt); + + // Assert + assertNotNull(token); + assertTrue(token instanceof CustomJwtAuthenticationToken); + + // Verify the token has the correct principal + EnhancedPrincipal principal = ((CustomJwtAuthenticationToken) token).getPrincipal(); + assertEquals("86a41a8a-ab2e-465e-8b48-a09d3275f842", principal.getSubject()); + assertEquals("management-node", principal.getClientId()); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/dbt/ndtp/ia/node/management/config/KeycloakJwtAuthenticationConverterTest.java b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/config/KeycloakJwtAuthenticationConverterTest.java new file mode 100644 index 0000000..a7ef272 --- /dev/null +++ b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/config/KeycloakJwtAuthenticationConverterTest.java @@ -0,0 +1,412 @@ +package uk.gov.dbt.ndtp.ia.node.management.config; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.RestTemplate; +import uk.gov.dbt.ndtp.ia.node.management.model.jwt.EnhancedPrincipal; +import uk.gov.dbt.ndtp.ia.node.management.model.jwt.JwtToken; + +import java.time.Instant; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class KeycloakJwtAuthenticationConverterTest { + + @Mock + private RestTemplate restTemplate; + + @InjectMocks + private KeycloakJwtAuthenticationConverter converter; + + private Jwt mockJwt; + private Map mockIntrospectionResponse; + + @BeforeEach + void setUp() { + // Set up configuration properties + ReflectionTestUtils.setField(converter, "introspectionUri", "http://localhost:8080/realms/management-node/protocol/openid-connect/token/introspect"); + ReflectionTestUtils.setField(converter, "clientId", "management-node"); + ReflectionTestUtils.setField(converter, "clientSecret", "0T5S4wNAPaaOUzFVFQyenorSEC6zxcb0"); + + // Create a mock JWT with the sample token data + Map headers = new HashMap<>(); + headers.put("alg", "RS256"); + headers.put("typ", "JWT"); + + Map claims = new HashMap<>(); + claims.put("exp", 1753576065); + claims.put("iat", 1753575765); + claims.put("jti", "trrtcc:713a03f4-55fb-4198-e8ea-a1be37d5f52f"); + claims.put("iss", "http://localhost:8080/realms/management-node"); + claims.put("sub", "86a41a8a-ab2e-465e-8b48-a09d3275f842"); + claims.put("typ", "Bearer"); + claims.put("azp", "management-node"); + claims.put("client_id", "management-node"); + + // Set up the aud claim as a list + List audiences = Arrays.asList("F1", "F2"); + claims.put("aud", audiences); + + // Set up the allowed-origins claim + List allowedOrigins = Collections.singletonList("/*"); + claims.put("allowed-origins", allowedOrigins); + + // Set up the resource_access claim with nested roles + Map resourceAccess = new HashMap<>(); + + // F1 resource + Map f1Resource = new HashMap<>(); + List f1Roles = Arrays.asList("TOPIC_2", "TOPIC_1"); + f1Resource.put("roles", f1Roles); + resourceAccess.put("F1", f1Resource); + + // management-node resource + Map managementNodeResource = new HashMap<>(); + List managementNodeRoles = Collections.singletonList("MyRole"); + managementNodeResource.put("roles", managementNodeRoles); + resourceAccess.put("management-node", managementNodeResource); + + // F2 resource + Map f2Resource = new HashMap<>(); + List f2Roles = Collections.singletonList("R1"); + f2Resource.put("roles", f2Roles); + resourceAccess.put("F2", f2Resource); + + claims.put("resource_access", resourceAccess); + + // Additional claims + claims.put("scope", "Sample_ORG management-node-client-scope"); + claims.put("clientHost", "172.20.0.1"); + claims.put("clientAddress", "172.20.0.1"); + + // Create the JWT with the headers and claims + mockJwt = new Jwt( + "token-value", + Instant.ofEpochSecond(1753575765), + Instant.ofEpochSecond(1753576065), + headers, + claims + ); + + // Create mock introspection response + mockIntrospectionResponse = new HashMap<>(); + mockIntrospectionResponse.put("active", true); + mockIntrospectionResponse.put("exp", 1753576065); + mockIntrospectionResponse.put("iat", 1753575765); + mockIntrospectionResponse.put("jti", "trrtcc:713a03f4-55fb-4198-e8ea-a1be37d5f52f"); + mockIntrospectionResponse.put("iss", "http://localhost:8080/realms/management-node"); + mockIntrospectionResponse.put("sub", "86a41a8a-ab2e-465e-8b48-a09d3275f842"); + mockIntrospectionResponse.put("typ", "Bearer"); + mockIntrospectionResponse.put("azp", "management-node"); + mockIntrospectionResponse.put("client_id", "management-node"); + mockIntrospectionResponse.put("aud", audiences); + mockIntrospectionResponse.put("allowed-origins", allowedOrigins); + mockIntrospectionResponse.put("resource_access", resourceAccess); + mockIntrospectionResponse.put("scope", "Sample_ORG management-node-client-scope"); + mockIntrospectionResponse.put("clientHost", "172.20.0.1"); + mockIntrospectionResponse.put("clientAddress", "172.20.0.1"); + + // Inject mock RestTemplate + ReflectionTestUtils.setField(converter, "restTemplate", restTemplate); + } + + private void setupMockIntrospectionResponse(Map response) { + // Convert Map to JwtToken + JwtToken jwtToken = createJwtTokenFromMap(response); + ResponseEntity responseEntity = new ResponseEntity<>(jwtToken, HttpStatus.OK); + Mockito.lenient().when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), Mockito.eq(JwtToken.class))) + .thenReturn(responseEntity); + } + + private JwtToken createJwtTokenFromMap(Map map) { + if (map == null) { + return null; + } + + // Extract resource_access and convert it to the format expected by JwtToken + Map resourceAccessMap = (Map) map.get("resource_access"); + Map resourceAccess = new HashMap<>(); + + if (resourceAccessMap != null) { + resourceAccessMap.forEach((resource, resourceDataObj) -> { + if (resourceDataObj instanceof Map) { + Map resourceData = (Map) resourceDataObj; + List roles = (List) resourceData.get("roles"); + if (roles != null) { + resourceAccess.put(resource, new JwtToken.ResourceAccess(roles)); + } + } + }); + } + + // Extract other fields + // Handle numeric values that could be Integer or Long + Long exp = null; + if (map.get("exp") != null) { + exp = map.get("exp") instanceof Long ? (Long) map.get("exp") : ((Number) map.get("exp")).longValue(); + } + + Long iat = null; + if (map.get("iat") != null) { + iat = map.get("iat") instanceof Long ? (Long) map.get("iat") : ((Number) map.get("iat")).longValue(); + } + + return JwtToken.builder() + .active((Boolean) map.get("active")) + .exp(exp) + .iat(iat) + .jti((String) map.get("jti")) + .iss((String) map.get("iss")) + .sub((String) map.get("sub")) + .typ((String) map.get("typ")) + .azp((String) map.get("azp")) + .clientId((String) map.get("client_id")) + .aud((List) map.get("aud")) + .allowedOrigins((List) map.get("allowed-origins")) + .resourceAccess(resourceAccess) + .scope((String) map.get("scope")) + .username((String) map.get("username")) + .tokenType((String) map.get("token_type")) + .build(); + } + + @Test + void convert_shouldExtractCorrectAuthorities() { + // Setup mock introspection response + setupMockIntrospectionResponse(mockIntrospectionResponse); + + // Act + AbstractAuthenticationToken token = converter.convert(mockJwt); + + // Assert + assertNotNull(token); + assertTrue(token instanceof CustomJwtAuthenticationToken); + assertEquals("86a41a8a-ab2e-465e-8b48-a09d3275f842", token.getName()); + + // Verify the CustomPrincipal has the correct clientId + EnhancedPrincipal principal = ((CustomJwtAuthenticationToken) token).getPrincipal(); + assertNotNull(principal); + assertEquals("86a41a8a-ab2e-465e-8b48-a09d3275f842", principal.getSubject()); + assertEquals("management-node", principal.getClientId()); + + Collection authorities = token.getAuthorities(); + assertNotNull(authorities); + + // Verify the expected authorities are present + List expectedAuthorities = Arrays.asList( + "ROLE_F1:TOPIC_1", + "ROLE_F1:TOPIC_2", + "ROLE_F2:R1", + "ROLE_management-node:MyRole" + ); + + // Don't assert the exact count as JwtGrantedAuthoritiesConverter may add default authorities + // Just verify that all our expected authorities are present + + for (String expectedAuthority : expectedAuthorities) { + boolean found = authorities.stream() + .anyMatch(authority -> authority.getAuthority().equals(expectedAuthority)); + assertTrue(found, "Expected authority not found: " + expectedAuthority); + } + } + + @Test + void convert_withNullResourceAccess_shouldNotFail() { + // Arrange + Map claims = new HashMap<>(); + claims.put("sub", "test-subject"); + + Jwt jwtWithoutResourceAccess = new Jwt( + "token-value", + Instant.now(), + Instant.now().plusSeconds(300), + Collections.singletonMap("alg", "none"), + claims + ); + + // Setup mock introspection response to return null (simulate introspection failure) + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), Mockito.eq(JwtToken.class))) + .thenThrow(new RuntimeException("Simulated introspection failure")); + + // Act + AbstractAuthenticationToken token = converter.convert(jwtWithoutResourceAccess); + + // Assert + assertNotNull(token); + assertTrue(token instanceof CustomJwtAuthenticationToken); + assertEquals("test-subject", token.getName()); + + // Verify the CustomPrincipal has the correct values + EnhancedPrincipal principal = ((CustomJwtAuthenticationToken) token).getPrincipal(); + assertNotNull(principal); + assertEquals("test-subject", principal.getSubject()); + assertEquals("unknown", principal.getClientId()); // Should default to "unknown" + + // Should not throw exception and return token with default authorities + } + + @Test + void convert_withEmptyRoles_shouldNotAddAuthorities() { + // Arrange + Map claims = new HashMap<>(); + claims.put("sub", "test-subject"); + + Map resourceAccess = new HashMap<>(); + Map resource = new HashMap<>(); + resource.put("roles", Collections.emptyList()); + resourceAccess.put("test-resource", resource); + claims.put("resource_access", resourceAccess); + + Jwt jwtWithEmptyRoles = new Jwt( + "token-value", + Instant.now(), + Instant.now().plusSeconds(300), + Collections.singletonMap("alg", "none"), + claims + ); + + // Setup mock introspection response to return null (simulate introspection failure) + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), Mockito.eq(JwtToken.class))) + .thenThrow(new RuntimeException("Simulated introspection failure")); + + // Act + AbstractAuthenticationToken token = converter.convert(jwtWithEmptyRoles); + + // Assert + assertNotNull(token); + assertTrue(token instanceof CustomJwtAuthenticationToken); + + // Verify the CustomPrincipal has the correct values + EnhancedPrincipal principal = ((CustomJwtAuthenticationToken) token).getPrincipal(); + assertNotNull(principal); + assertEquals("test-subject", principal.getSubject()); + assertEquals("unknown", principal.getClientId()); // Should default to "unknown" + + // Should not add any authorities for the empty roles list + assertEquals(0, token.getAuthorities().size()); + } + + @Test + void convert_withMalformedResourceAccess_shouldHandleGracefully() { + // Arrange + Map claims = new HashMap<>(); + claims.put("sub", "test-subject"); + + // Malformed resource_access (not a map) + claims.put("resource_access", "not-a-map"); + + Jwt jwtWithMalformedResourceAccess = new Jwt( + "token-value", + Instant.now(), + Instant.now().plusSeconds(300), + Collections.singletonMap("alg", "none"), + claims + ); + + // Setup mock introspection response to return null (simulate introspection failure) + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), Mockito.eq(JwtToken.class))) + .thenThrow(new RuntimeException("Simulated introspection failure")); + + // Act & Assert + // Should not throw exception + AbstractAuthenticationToken token = converter.convert(jwtWithMalformedResourceAccess); + assertNotNull(token); + assertTrue(token instanceof CustomJwtAuthenticationToken); + + // Verify the CustomPrincipal has the correct values + EnhancedPrincipal principal = ((CustomJwtAuthenticationToken) token).getPrincipal(); + assertNotNull(principal); + assertEquals("test-subject", principal.getSubject()); + assertEquals("unknown", principal.getClientId()); // Should default to "unknown" + } + + @Test + void convert_shouldUseIntrospectionEndpoint() { + // Setup mock introspection response + setupMockIntrospectionResponse(mockIntrospectionResponse); + + // Act + AbstractAuthenticationToken token = converter.convert(mockJwt); + + // Assert + // Verify that the RestTemplate was called + Mockito.verify(restTemplate).postForEntity( + anyString(), + any(HttpEntity.class), + Mockito.>any() + ); + + // Verify the token is correct + assertNotNull(token); + assertTrue(token instanceof CustomJwtAuthenticationToken); + assertEquals("86a41a8a-ab2e-465e-8b48-a09d3275f842", token.getName()); + + // Verify the CustomPrincipal has the correct clientId + EnhancedPrincipal principal = ((CustomJwtAuthenticationToken) token).getPrincipal(); + assertNotNull(principal); + assertEquals("86a41a8a-ab2e-465e-8b48-a09d3275f842", principal.getSubject()); + assertEquals("management-node", principal.getClientId()); + + // Verify the authorities + Collection authorities = token.getAuthorities(); + assertNotNull(authorities); + + // Verify the expected authorities are present + List expectedAuthorities = Arrays.asList( + "ROLE_F1:TOPIC_1", + "ROLE_F1:TOPIC_2", + "ROLE_F2:R1", + "ROLE_management-node:MyRole" + ); + + for (String expectedAuthority : expectedAuthorities) { + boolean found = authorities.stream() + .anyMatch(authority -> authority.getAuthority().equals(expectedAuthority)); + assertTrue(found, "Expected authority not found: " + expectedAuthority); + } + } + + @Test + void convert_withIntrospectionFailure_shouldFallbackToJwtParsing() { + // Arrange + // Configure RestTemplate to throw an exception + Mockito.reset(restTemplate); // Reset any previous stubbing + when(restTemplate.postForEntity( + anyString(), + any(HttpEntity.class), + Mockito.eq(JwtToken.class) + )).thenThrow(new RuntimeException("Introspection failed")); + + // Act + AbstractAuthenticationToken token = converter.convert(mockJwt); + + // Assert + // Verify that the token is still created using JWT parsing + assertNotNull(token); + assertTrue(token instanceof CustomJwtAuthenticationToken); + assertEquals("86a41a8a-ab2e-465e-8b48-a09d3275f842", token.getName()); + + // Verify the CustomPrincipal has the correct clientId + EnhancedPrincipal principal = ((CustomJwtAuthenticationToken) token).getPrincipal(); + assertNotNull(principal); + assertEquals("86a41a8a-ab2e-465e-8b48-a09d3275f842", principal.getSubject()); + assertEquals("management-node", principal.getClientId()); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/dbt/ndtp/ia/node/management/controller/v1/ConfigurationControllerTest.java b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/controller/v1/ConfigurationControllerTest.java new file mode 100644 index 0000000..6d5dda1 --- /dev/null +++ b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/controller/v1/ConfigurationControllerTest.java @@ -0,0 +1,131 @@ +package uk.gov.dbt.ndtp.ia.node.management.controller.v1; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ConsumerConfigDTO; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProducerConfigDTO; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProducerDTO; +import uk.gov.dbt.ndtp.ia.node.management.service.providers.configuration.ConfigurationProvider; + +import java.util.ArrayList; +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +class ConfigurationControllerTest { + + private MockMvc mockMvc; + + @Mock + private ConfigurationProvider configurationProvider; + + @InjectMocks + private ConfigurationController configurationController; + + private final String CLIENT_ID = "test-client-id"; + private final Long PRODUCER_ID = 1L; + private final Long CONSUMER_ID = 2L; + + private ProducerConfigDTO producerConfigDTO; + private ConsumerConfigDTO consumerConfigDTO; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(configurationController) + .build(); + + // Set up producer config + ProducerDTO producerDTO = ProducerDTO.builder() + .id(PRODUCER_ID) + .name("Test Producer") + .active(true) + .build(); + + producerConfigDTO = ProducerConfigDTO.builder() + .clientId(CLIENT_ID) + .producers(Collections.singletonList(producerDTO)) + .build(); + + // Set up consumer config + consumerConfigDTO = ConsumerConfigDTO.builder() + .clientId(CLIENT_ID) + .producers(new ArrayList<>()) + .build(); + } + + // Note: In a real test environment with a full Spring Security context, we would use: + // 1. @WithMockUser to test authorization rules + // 2. SecurityMockMvcRequestPostProcessors.user() to provide authentication + // 3. A WebMvcTest with a proper security configuration + // + // For this example, we're using a standalone setup that bypasses Spring Security, + // so we're focusing on testing the controller functionality rather than authorization. + // The actual authorization is enforced by Spring Security through the @PreAuthorize annotations. + + @Test + void getProducerConfigurations_shouldReturnConfig() throws Exception { + // Arrange + when(configurationProvider.getProducerConfigByClientId(any(), any())) + .thenReturn(producerConfigDTO); + + // Act & Assert + mockMvc.perform(get("/api/v1/configuration/producer") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.clientId").value(CLIENT_ID)); + } + + @Test + void getProducerConfigurations_withProducerId_shouldReturnFilteredConfig() throws Exception { + // Arrange + when(configurationProvider.getProducerConfigByClientId(any(), any())) + .thenReturn(producerConfigDTO); + + // Act & Assert + mockMvc.perform(get("/api/v1/configuration/producer") + .param("producer_id", PRODUCER_ID.toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.clientId").value(CLIENT_ID)); + } + + @Test + void getConsumerConfigurations_shouldReturnConfig() throws Exception { + // Arrange + when(configurationProvider.getConsumerConfigByClientId(any(), any())) + .thenReturn(consumerConfigDTO); + + // Act & Assert + mockMvc.perform(get("/api/v1/configuration/consumer") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.clientId").value(CLIENT_ID)); + } + + @Test + void getConsumerConfigurations_withConsumerId_shouldReturnFilteredConfig() throws Exception { + // Arrange + when(configurationProvider.getConsumerConfigByClientId(any(), any())) + .thenReturn(consumerConfigDTO); + + // Act & Assert + mockMvc.perform(get("/api/v1/configuration/consumer") + .param("consumer_id", CONSUMER_ID.toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.clientId").value(CLIENT_ID)); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ConsumerConverterTest.java b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ConsumerConverterTest.java new file mode 100644 index 0000000..397a230 --- /dev/null +++ b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ConsumerConverterTest.java @@ -0,0 +1,165 @@ +package uk.gov.dbt.ndtp.ia.node.management.converter.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ConsumerDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Organisation; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Consumer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.OrganisationRepository; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ConsumerConverterTest { + + @Mock + private OrganisationRepository organisationRepository; + + @InjectMocks + private ConsumerConverter converter; + + private Consumer entity; + private ConsumerDTO dto; + private Organisation organisation; + + private final Long consumerId = 1L; + private final String consumerName = "Test Consumer"; + private final String idpClientId = "test-client-id"; + private final Long orgId = 101L; + private final String orgName = "Test Organisation"; + + @BeforeEach + void setUp() { + // Create test organisation + organisation = new Organisation(); + organisation.setId(orgId); + organisation.setName(orgName); + + // Create test entity + entity = new Consumer(); + entity.setId(consumerId); + entity.setName(consumerName); + entity.setIdpClientId(idpClientId); + entity.setOrg(organisation); + + // Create test DTO + dto = new ConsumerDTO(); + dto.setId(consumerId); + dto.setName(consumerName); + dto.setIdpClientId(idpClientId); + dto.setOrgId(orgId); + } + + @Test + void toDto_withNullEntity_shouldReturnNull() { + // Act + ConsumerDTO result = converter.toDto(null); + + // Assert + assertNull(result); + } + + @Test + void toDto_withValidEntity_shouldReturnCorrectDTO() { + // Act + ConsumerDTO result = converter.toDto(entity); + + // Assert + assertNotNull(result); + assertEquals(consumerId, result.getId()); + assertEquals(consumerName, result.getName()); + assertEquals(idpClientId, result.getIdpClientId()); + assertEquals(orgId, result.getOrgId()); + } + + @Test + void toDto_withNullOrg_shouldReturnDTOWithNullOrgId() { + // Arrange + entity.setOrg(null); + + // Act + ConsumerDTO result = converter.toDto(entity); + + // Assert + assertNotNull(result); + assertEquals(consumerId, result.getId()); + assertEquals(consumerName, result.getName()); + assertEquals(idpClientId, result.getIdpClientId()); + assertNull(result.getOrgId()); + } + + @Test + void toEntity_withNullDTO_shouldReturnNull() { + // Act + Consumer result = converter.toEntity(null); + + // Assert + assertNull(result); + } + + @Test + void toEntity_withValidDTO_shouldReturnCorrectEntity() { + // Arrange + when(organisationRepository.findById(orgId)).thenReturn(Optional.of(organisation)); + + // Act + Consumer result = converter.toEntity(dto); + + // Assert + assertNotNull(result); + assertEquals(consumerId, result.getId()); + assertEquals(consumerName, result.getName()); + assertEquals(idpClientId, result.getIdpClientId()); + assertNotNull(result.getOrg()); + assertEquals(orgId, result.getOrg().getId()); + assertEquals(orgName, result.getOrg().getName()); + + // Verify + verify(organisationRepository, times(1)).findById(orgId); + } + + @Test + void toEntity_withNullOrgId_shouldReturnEntityWithNullOrg() { + // Arrange + dto.setOrgId(null); + + // Act + Consumer result = converter.toEntity(dto); + + // Assert + assertNotNull(result); + assertEquals(consumerId, result.getId()); + assertEquals(consumerName, result.getName()); + assertEquals(idpClientId, result.getIdpClientId()); + assertNull(result.getOrg()); + + // Verify + verify(organisationRepository, never()).findById(any()); + } + + @Test + void toEntity_withNonExistentOrgId_shouldReturnEntityWithNullOrg() { + // Arrange + when(organisationRepository.findById(orgId)).thenReturn(Optional.empty()); + + // Act + Consumer result = converter.toEntity(dto); + + // Assert + assertNotNull(result); + assertEquals(consumerId, result.getId()); + assertEquals(consumerName, result.getName()); + assertEquals(idpClientId, result.getIdpClientId()); + assertNull(result.getOrg()); + + // Verify + verify(organisationRepository, times(1)).findById(orgId); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/OrganisationProducerConverterTest.java b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/OrganisationProducerConverterTest.java new file mode 100644 index 0000000..fdd2335 --- /dev/null +++ b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/OrganisationProducerConverterTest.java @@ -0,0 +1,413 @@ +package uk.gov.dbt.ndtp.ia.node.management.converter.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProductDTO; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProducerDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Product; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Organisation; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Producer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.OrganisationRepository; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class OrganisationProducerConverterTest { + + @Mock + private OrganisationRepository organisationRepository; + + @Mock + private ProductConverter productConverter; + + @InjectMocks + private OrganisationProducerConverter converter; + + private Producer entity; + private ProducerDTO dto; + private Organisation organisation; + private List dataProviders; + private List dataProviderDTOs; + + private final Long producerId = 1L; + private final String producerName = "Test Producer"; + private final String description = "Test Description"; + private final Boolean active = true; + private final String host = "test-host"; + private final BigDecimal port = new BigDecimal("8080"); + private final Boolean tls = true; + private final String idpClientId = "test-client-id"; + private final Long orgId = 101L; + private final String orgName = "Test Organisation"; + + private final Long dataProviderId1 = 201L; + private final String dataProviderName1 = "Test Data Provider 1"; + private final String topic1 = "test-topic-1"; + + private final Long dataProviderId2 = 202L; + private final String dataProviderName2 = "Test Data Provider 2"; + private final String topic2 = "test-topic-2"; + + @BeforeEach + void setUp() { + // Create test organisation + organisation = new Organisation(); + organisation.setId(orgId); + organisation.setName(orgName); + + // Create test data providers + dataProviders = new ArrayList<>(); + + Product dataProvider1 = new Product(); + dataProvider1.setId(dataProviderId1); + dataProvider1.setName(dataProviderName1); + dataProvider1.setTopic(topic1); + + Product dataProvider2 = new Product(); + dataProvider2.setId(dataProviderId2); + dataProvider2.setName(dataProviderName2); + dataProvider2.setTopic(topic2); + + dataProviders.add(dataProvider1); + dataProviders.add(dataProvider2); + + // Create test entity + entity = new Producer(); + entity.setId(producerId); + entity.setName(producerName); + entity.setDescription(description); + entity.setActive(active); + entity.setHost(host); + entity.setPort(port); + entity.setTls(tls); + entity.setIdpClientId(idpClientId); + entity.setOrg(organisation); + entity.setProducts(dataProviders); + + // Set producer reference in data providers + dataProvider1.setProducer(entity); + dataProvider2.setProducer(entity); + + // Create test data provider DTOs + dataProviderDTOs = new ArrayList<>(); + + ProductDTO dataProviderDTO1 = ProductDTO.builder() + .id(dataProviderId1) + .name(dataProviderName1) + .topic(topic1) + .producerId(producerId) + .build(); + + ProductDTO dataProviderDTO2 = ProductDTO.builder() + .id(dataProviderId2) + .name(dataProviderName2) + .topic(topic2) + .producerId(producerId) + .build(); + + dataProviderDTOs.add(dataProviderDTO1); + dataProviderDTOs.add(dataProviderDTO2); + + // Create test DTO + dto = new ProducerDTO(); + dto.setId(producerId); + dto.setName(producerName); + dto.setDescription(description); + dto.setActive(active); + dto.setHost(host); + dto.setPort(port); + dto.setTls(tls); + dto.setIdpClientId(idpClientId); + dto.setOrgId(orgId); + + // Add data provider DTOs to the producer DTO + dto.getDataProviders().addAll(dataProviderDTOs); + + // Set up mock behavior for productConverter + lenient().when(productConverter.toDto(dataProvider1)).thenReturn(dataProviderDTO1); + lenient().when(productConverter.toDto(dataProvider2)).thenReturn(dataProviderDTO2); + lenient().when(productConverter.toEntity(dataProviderDTO1)).thenReturn(dataProvider1); + lenient().when(productConverter.toEntity(dataProviderDTO2)).thenReturn(dataProvider2); + } + + @Test + void toDto_withNullEntity_shouldReturnNull() { + // Act + ProducerDTO result = converter.toDto(null); + + // Assert + assertNull(result); + } + + @Test + void toDto_withValidEntity_shouldReturnCorrectDTO() { + // Act + ProducerDTO result = converter.toDto(entity); + + // Assert + assertNotNull(result); + assertEquals(producerId, result.getId()); + assertEquals(producerName, result.getName()); + assertEquals(description, result.getDescription()); + assertEquals(active, result.getActive()); + assertEquals(host, result.getHost()); + assertEquals(port, result.getPort()); + assertEquals(tls, result.getTls()); + assertEquals(idpClientId, result.getIdpClientId()); + assertEquals(orgId, result.getOrgId()); + + // Verify dataProviders mapping + assertNotNull(result.getDataProviders()); + assertEquals(2, result.getDataProviders().size()); + + // Verify first data provider + ProductDTO productDTO1 = result.getDataProviders().get(0); + assertEquals(dataProviderId1, productDTO1.getId()); + assertEquals(dataProviderName1, productDTO1.getName()); + assertEquals(topic1, productDTO1.getTopic()); + assertEquals(producerId, productDTO1.getProducerId()); + + // Verify second data provider + ProductDTO dataProviderDTO2 = result.getDataProviders().get(1); + assertEquals(dataProviderId2, dataProviderDTO2.getId()); + assertEquals(dataProviderName2, dataProviderDTO2.getName()); + assertEquals(topic2, dataProviderDTO2.getTopic()); + assertEquals(producerId, dataProviderDTO2.getProducerId()); + + // Verify productConverter was called for each data provider + verify(productConverter, times(1)).toDto(dataProviders.get(0)); + verify(productConverter, times(1)).toDto(dataProviders.get(1)); + } + + @Test + void toDto_withNullOrg_shouldReturnDTOWithNullOrgId() { + // Arrange + entity.setOrg(null); + + // Act + ProducerDTO result = converter.toDto(entity); + + // Assert + assertNotNull(result); + assertEquals(producerId, result.getId()); + assertEquals(producerName, result.getName()); + assertEquals(description, result.getDescription()); + assertEquals(active, result.getActive()); + assertEquals(host, result.getHost()); + assertEquals(port, result.getPort()); + assertEquals(tls, result.getTls()); + assertEquals(idpClientId, result.getIdpClientId()); + assertNull(result.getOrgId()); + } + + @Test + void toDto_withNullProducts_shouldReturnDTOWithEmptyDataProviders() { + // Arrange + entity.setProducts(null); + + // Act + ProducerDTO result = converter.toDto(entity); + + // Assert + assertNotNull(result); + assertNotNull(result.getDataProviders()); + assertTrue(result.getDataProviders().isEmpty()); + + // Verify productConverter was not called + verify(productConverter, never()).toDto(any()); + } + + @Test + void toDto_withEmptyProducts_shouldReturnDTOWithEmptyDataProviders() { + // Arrange + entity.setProducts(new ArrayList<>()); + + // Act + ProducerDTO result = converter.toDto(entity); + + // Assert + assertNotNull(result); + assertNotNull(result.getDataProviders()); + assertTrue(result.getDataProviders().isEmpty()); + + // Verify productConverter was not called + verify(productConverter, never()).toDto(any()); + } + + @Test + void toEntity_withNullDTO_shouldReturnNull() { + // Act + Producer result = converter.toEntity(null); + + // Assert + assertNull(result); + } + + @Test + void toEntity_withValidDTO_shouldReturnCorrectEntity() { + // Arrange + when(organisationRepository.findById(orgId)).thenReturn(Optional.of(organisation)); + + // Act + Producer result = converter.toEntity(dto); + + // Assert + assertNotNull(result); + assertEquals(producerId, result.getId()); + assertEquals(producerName, result.getName()); + assertEquals(description, result.getDescription()); + assertEquals(active, result.getActive()); + assertEquals(host, result.getHost()); + assertEquals(port, result.getPort()); + assertEquals(tls, result.getTls()); + assertEquals(idpClientId, result.getIdpClientId()); + assertNotNull(result.getOrg()); + assertEquals(orgId, result.getOrg().getId()); + assertEquals(orgName, result.getOrg().getName()); + + // Verify dataProviders mapping + assertNotNull(result.getProducts()); + assertEquals(2, result.getProducts().size()); + + // Verify first data provider + Product dataProvider1 = result.getProducts().get(0); + assertEquals(dataProviderId1, dataProvider1.getId()); + assertEquals(dataProviderName1, dataProvider1.getName()); + assertEquals(topic1, dataProvider1.getTopic()); + assertNotNull(dataProvider1.getProducer()); + assertEquals(result, dataProvider1.getProducer()); + + // Verify second data provider + Product dataProvider2 = result.getProducts().get(1); + assertEquals(dataProviderId2, dataProvider2.getId()); + assertEquals(dataProviderName2, dataProvider2.getName()); + assertEquals(topic2, dataProvider2.getTopic()); + assertNotNull(dataProvider2.getProducer()); + assertEquals(result, dataProvider2.getProducer()); + + // Verify productConverter was called for each data provider DTO + verify(productConverter, times(1)).toEntity(dataProviderDTOs.get(0)); + verify(productConverter, times(1)).toEntity(dataProviderDTOs.get(1)); + + // Verify organisation repository was called + verify(organisationRepository, times(1)).findById(orgId); + } + + @Test + void toEntity_withNullOrgId_shouldReturnEntityWithNullOrg() { + // Arrange + dto.setOrgId(null); + + // Act + Producer result = converter.toEntity(dto); + + // Assert + assertNotNull(result); + assertEquals(producerId, result.getId()); + assertEquals(producerName, result.getName()); + assertEquals(description, result.getDescription()); + assertEquals(active, result.getActive()); + assertEquals(host, result.getHost()); + assertEquals(port, result.getPort()); + assertEquals(tls, result.getTls()); + assertEquals(idpClientId, result.getIdpClientId()); + assertNull(result.getOrg()); + + // Verify + verify(organisationRepository, never()).findById(any()); + } + + @Test + void toEntity_withNonExistentOrgId_shouldReturnEntityWithNullOrg() { + // Arrange + when(organisationRepository.findById(orgId)).thenReturn(Optional.empty()); + + // Act + Producer result = converter.toEntity(dto); + + // Assert + assertNotNull(result); + assertEquals(producerId, result.getId()); + assertEquals(producerName, result.getName()); + assertEquals(description, result.getDescription()); + assertEquals(active, result.getActive()); + assertEquals(host, result.getHost()); + assertEquals(port, result.getPort()); + assertEquals(tls, result.getTls()); + assertEquals(idpClientId, result.getIdpClientId()); + assertNull(result.getOrg()); + + // Verify + verify(organisationRepository, times(1)).findById(orgId); + } + + @Test + void toEntity_withEmptyDataProviders_shouldReturnEntityWithEmptyProducts() { + // Arrange + dto.getDataProviders().clear(); + + // Act + Producer result = converter.toEntity(dto); + + // Assert + assertNotNull(result); + assertNull(result.getProducts()); + + // Verify productConverter was not called + verify(productConverter, never()).toEntity(any()); + } + + @Test + void toEntity_withNullProducerId_shouldSetProducerIdInDataProviderDTO() { + // Arrange + when(organisationRepository.findById(orgId)).thenReturn(Optional.of(organisation)); + + // Set producerId to null in data provider DTOs + dataProviderDTOs.get(0).setProducerId(null); + dataProviderDTOs.get(1).setProducerId(null); + + // Act + Producer result = converter.toEntity(dto); + + // Assert + assertNotNull(result); + assertNotNull(result.getProducts()); + assertEquals(2, result.getProducts().size()); + + // Verify producerId was set in data provider DTOs + verify(productConverter, times(1)).toEntity(dataProviderDTOs.get(0)); + verify(productConverter, times(1)).toEntity(dataProviderDTOs.get(1)); + + // Verify producerId was set in data provider DTOs + assertEquals(producerId, dataProviderDTOs.get(0).getProducerId()); + assertEquals(producerId, dataProviderDTOs.get(1).getProducerId()); + } + + @Test + void toEntity_withNullDataProviderFromConverter_shouldNotAddToProducts() { + // Arrange + when(organisationRepository.findById(orgId)).thenReturn(Optional.of(organisation)); + when(productConverter.toEntity(dataProviderDTOs.get(1))).thenReturn(null); + + // Act + Producer result = converter.toEntity(dto); + + // Assert + assertNotNull(result); + assertNotNull(result.getProducts()); + assertEquals(1, result.getProducts().size()); + + // Verify only one data provider was added + assertEquals(dataProviderId1, result.getProducts().get(0).getId()); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProducerConverterTest.java b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProducerConverterTest.java new file mode 100644 index 0000000..aa4ad90 --- /dev/null +++ b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProducerConverterTest.java @@ -0,0 +1,413 @@ +package uk.gov.dbt.ndtp.ia.node.management.converter.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProductDTO; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProducerDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Product; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Organisation; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Producer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.OrganisationRepository; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ProducerConverterTest { + + @Mock + private OrganisationRepository organisationRepository; + + @Mock + private ProductConverter productConverter; + + @InjectMocks + private ProducerConverter converter; + + private Producer entity; + private ProducerDTO dto; + private Organisation organisation; + private List dataProviders; + private List dataProviderDTOs; + + private final Long producerId = 1L; + private final String producerName = "Test Producer"; + private final String description = "Test Description"; + private final Boolean active = true; + private final String host = "test-host"; + private final BigDecimal port = new BigDecimal("8080"); + private final Boolean tls = true; + private final String idpClientId = "test-client-id"; + private final Long orgId = 101L; + private final String orgName = "Test Organisation"; + + private final Long dataProviderId1 = 201L; + private final String dataProviderName1 = "Test Data Provider 1"; + private final String topic1 = "test-topic-1"; + + private final Long dataProviderId2 = 202L; + private final String dataProviderName2 = "Test Data Provider 2"; + private final String topic2 = "test-topic-2"; + + @BeforeEach + void setUp() { + // Create test organisation + organisation = new Organisation(); + organisation.setId(orgId); + organisation.setName(orgName); + + // Create test data providers + dataProviders = new ArrayList<>(); + + Product dataProvider1 = new Product(); + dataProvider1.setId(dataProviderId1); + dataProvider1.setName(dataProviderName1); + dataProvider1.setTopic(topic1); + + Product dataProvider2 = new Product(); + dataProvider2.setId(dataProviderId2); + dataProvider2.setName(dataProviderName2); + dataProvider2.setTopic(topic2); + + dataProviders.add(dataProvider1); + dataProviders.add(dataProvider2); + + // Create test entity + entity = new Producer(); + entity.setId(producerId); + entity.setName(producerName); + entity.setDescription(description); + entity.setActive(active); + entity.setHost(host); + entity.setPort(port); + entity.setTls(tls); + entity.setIdpClientId(idpClientId); + entity.setOrg(organisation); + entity.setProducts(dataProviders); + + // Set producer reference in data providers + dataProvider1.setProducer(entity); + dataProvider2.setProducer(entity); + + // Create test data provider DTOs + dataProviderDTOs = new ArrayList<>(); + + ProductDTO dataProviderDTO1 = ProductDTO.builder() + .id(dataProviderId1) + .name(dataProviderName1) + .topic(topic1) + .producerId(producerId) + .build(); + + ProductDTO dataProviderDTO2 = ProductDTO.builder() + .id(dataProviderId2) + .name(dataProviderName2) + .topic(topic2) + .producerId(producerId) + .build(); + + dataProviderDTOs.add(dataProviderDTO1); + dataProviderDTOs.add(dataProviderDTO2); + + // Create test DTO + dto = new ProducerDTO(); + dto.setId(producerId); + dto.setName(producerName); + dto.setDescription(description); + dto.setActive(active); + dto.setHost(host); + dto.setPort(port); + dto.setTls(tls); + dto.setIdpClientId(idpClientId); + dto.setOrgId(orgId); + + // Add data provider DTOs to the producer DTO + dto.getDataProviders().addAll(dataProviderDTOs); + + // Set up mock behavior for productConverter + lenient().when(productConverter.toDto(dataProvider1)).thenReturn(dataProviderDTO1); + lenient().when(productConverter.toDto(dataProvider2)).thenReturn(dataProviderDTO2); + lenient().when(productConverter.toEntity(dataProviderDTO1)).thenReturn(dataProvider1); + lenient().when(productConverter.toEntity(dataProviderDTO2)).thenReturn(dataProvider2); + } + + @Test + void toDto_withNullEntity_shouldReturnNull() { + // Act + ProducerDTO result = converter.toDto(null); + + // Assert + assertNull(result); + } + + @Test + void toDto_withValidEntity_shouldReturnCorrectDTO() { + // Act + ProducerDTO result = converter.toDto(entity); + + // Assert + assertNotNull(result); + assertEquals(producerId, result.getId()); + assertEquals(producerName, result.getName()); + assertEquals(description, result.getDescription()); + assertEquals(active, result.getActive()); + assertEquals(host, result.getHost()); + assertEquals(port, result.getPort()); + assertEquals(tls, result.getTls()); + assertEquals(idpClientId, result.getIdpClientId()); + assertEquals(orgId, result.getOrgId()); + + // Verify dataProviders mapping + assertNotNull(result.getDataProviders()); + assertEquals(2, result.getDataProviders().size()); + + // Verify first data provider + ProductDTO productDTO1 = result.getDataProviders().get(0); + assertEquals(dataProviderId1, productDTO1.getId()); + assertEquals(dataProviderName1, productDTO1.getName()); + assertEquals(topic1, productDTO1.getTopic()); + assertEquals(producerId, productDTO1.getProducerId()); + + // Verify second data provider + ProductDTO dataProviderDTO2 = result.getDataProviders().get(1); + assertEquals(dataProviderId2, dataProviderDTO2.getId()); + assertEquals(dataProviderName2, dataProviderDTO2.getName()); + assertEquals(topic2, dataProviderDTO2.getTopic()); + assertEquals(producerId, dataProviderDTO2.getProducerId()); + + // Verify productConverter was called for each data provider + verify(productConverter, times(1)).toDto(dataProviders.get(0)); + verify(productConverter, times(1)).toDto(dataProviders.get(1)); + } + + @Test + void toDto_withNullOrg_shouldReturnDTOWithNullOrgId() { + // Arrange + entity.setOrg(null); + + // Act + ProducerDTO result = converter.toDto(entity); + + // Assert + assertNotNull(result); + assertEquals(producerId, result.getId()); + assertEquals(producerName, result.getName()); + assertEquals(description, result.getDescription()); + assertEquals(active, result.getActive()); + assertEquals(host, result.getHost()); + assertEquals(port, result.getPort()); + assertEquals(tls, result.getTls()); + assertEquals(idpClientId, result.getIdpClientId()); + assertNull(result.getOrgId()); + } + + @Test + void toDto_withNullProducts_shouldReturnDTOWithEmptyDataProviders() { + // Arrange + entity.setProducts(null); + + // Act + ProducerDTO result = converter.toDto(entity); + + // Assert + assertNotNull(result); + assertNotNull(result.getDataProviders()); + assertTrue(result.getDataProviders().isEmpty()); + + // Verify productConverter was not called + verify(productConverter, never()).toDto(any()); + } + + @Test + void toDto_withEmptyProducts_shouldReturnDTOWithEmptyDataProviders() { + // Arrange + entity.setProducts(new ArrayList<>()); + + // Act + ProducerDTO result = converter.toDto(entity); + + // Assert + assertNotNull(result); + assertNotNull(result.getDataProviders()); + assertTrue(result.getDataProviders().isEmpty()); + + // Verify productConverter was not called + verify(productConverter, never()).toDto(any()); + } + + @Test + void toEntity_withNullDTO_shouldReturnNull() { + // Act + Producer result = converter.toEntity(null); + + // Assert + assertNull(result); + } + + @Test + void toEntity_withValidDTO_shouldReturnCorrectEntity() { + // Arrange + when(organisationRepository.findById(orgId)).thenReturn(Optional.of(organisation)); + + // Act + Producer result = converter.toEntity(dto); + + // Assert + assertNotNull(result); + assertEquals(producerId, result.getId()); + assertEquals(producerName, result.getName()); + assertEquals(description, result.getDescription()); + assertEquals(active, result.getActive()); + assertEquals(host, result.getHost()); + assertEquals(port, result.getPort()); + assertEquals(tls, result.getTls()); + assertEquals(idpClientId, result.getIdpClientId()); + assertNotNull(result.getOrg()); + assertEquals(orgId, result.getOrg().getId()); + assertEquals(orgName, result.getOrg().getName()); + + // Verify dataProviders mapping + assertNotNull(result.getProducts()); + assertEquals(2, result.getProducts().size()); + + // Verify first data provider + Product dataProvider1 = result.getProducts().get(0); + assertEquals(dataProviderId1, dataProvider1.getId()); + assertEquals(dataProviderName1, dataProvider1.getName()); + assertEquals(topic1, dataProvider1.getTopic()); + assertNotNull(dataProvider1.getProducer()); + assertEquals(result, dataProvider1.getProducer()); + + // Verify second data provider + Product dataProvider2 = result.getProducts().get(1); + assertEquals(dataProviderId2, dataProvider2.getId()); + assertEquals(dataProviderName2, dataProvider2.getName()); + assertEquals(topic2, dataProvider2.getTopic()); + assertNotNull(dataProvider2.getProducer()); + assertEquals(result, dataProvider2.getProducer()); + + // Verify productConverter was called for each data provider DTO + verify(productConverter, times(1)).toEntity(dataProviderDTOs.get(0)); + verify(productConverter, times(1)).toEntity(dataProviderDTOs.get(1)); + + // Verify organisation repository was called + verify(organisationRepository, times(1)).findById(orgId); + } + + @Test + void toEntity_withNullOrgId_shouldReturnEntityWithNullOrg() { + // Arrange + dto.setOrgId(null); + + // Act + Producer result = converter.toEntity(dto); + + // Assert + assertNotNull(result); + assertEquals(producerId, result.getId()); + assertEquals(producerName, result.getName()); + assertEquals(description, result.getDescription()); + assertEquals(active, result.getActive()); + assertEquals(host, result.getHost()); + assertEquals(port, result.getPort()); + assertEquals(tls, result.getTls()); + assertEquals(idpClientId, result.getIdpClientId()); + assertNull(result.getOrg()); + + // Verify + verify(organisationRepository, never()).findById(any()); + } + + @Test + void toEntity_withNonExistentOrgId_shouldReturnEntityWithNullOrg() { + // Arrange + when(organisationRepository.findById(orgId)).thenReturn(Optional.empty()); + + // Act + Producer result = converter.toEntity(dto); + + // Assert + assertNotNull(result); + assertEquals(producerId, result.getId()); + assertEquals(producerName, result.getName()); + assertEquals(description, result.getDescription()); + assertEquals(active, result.getActive()); + assertEquals(host, result.getHost()); + assertEquals(port, result.getPort()); + assertEquals(tls, result.getTls()); + assertEquals(idpClientId, result.getIdpClientId()); + assertNull(result.getOrg()); + + // Verify + verify(organisationRepository, times(1)).findById(orgId); + } + + @Test + void toEntity_withEmptyDataProviders_shouldReturnEntityWithEmptyProducts() { + // Arrange + dto.getDataProviders().clear(); + + // Act + Producer result = converter.toEntity(dto); + + // Assert + assertNotNull(result); + assertNull(result.getProducts()); + + // Verify productConverter was not called + verify(productConverter, never()).toEntity(any()); + } + + @Test + void toEntity_withNullProducerId_shouldSetProducerIdInDataProviderDTO() { + // Arrange + when(organisationRepository.findById(orgId)).thenReturn(Optional.of(organisation)); + + // Set producerId to null in data provider DTOs + dataProviderDTOs.get(0).setProducerId(null); + dataProviderDTOs.get(1).setProducerId(null); + + // Act + Producer result = converter.toEntity(dto); + + // Assert + assertNotNull(result); + assertNotNull(result.getProducts()); + assertEquals(2, result.getProducts().size()); + + // Verify producerId was set in data provider DTOs + verify(productConverter, times(1)).toEntity(dataProviderDTOs.get(0)); + verify(productConverter, times(1)).toEntity(dataProviderDTOs.get(1)); + + // Verify producerId was set in data provider DTOs + assertEquals(producerId, dataProviderDTOs.get(0).getProducerId()); + assertEquals(producerId, dataProviderDTOs.get(1).getProducerId()); + } + + @Test + void toEntity_withNullDataProviderFromConverter_shouldNotAddToProducts() { + // Arrange + when(organisationRepository.findById(orgId)).thenReturn(Optional.of(organisation)); + when(productConverter.toEntity(dataProviderDTOs.get(1))).thenReturn(null); + + // Act + Producer result = converter.toEntity(dto); + + // Assert + assertNotNull(result); + assertNotNull(result.getProducts()); + assertEquals(1, result.getProducts().size()); + + // Verify only one data provider was added + assertEquals(dataProviderId1, result.getProducts().get(0).getId()); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProductConsumerConverterTest.java b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProductConsumerConverterTest.java new file mode 100644 index 0000000..c15916b --- /dev/null +++ b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProductConsumerConverterTest.java @@ -0,0 +1,94 @@ +package uk.gov.dbt.ndtp.ia.node.management.converter.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProductConsumerDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumerId; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class ProductConsumerConverterTest { + + @InjectMocks + private ProductConsumerConverter converter; + + private ProductConsumer entity; + private ProductConsumerDTO dto; + private final Long consumerId = 1L; + private final Long dataProviderId = 101L; + private final Timestamp grantedTs = Timestamp.from(Instant.now()); + private final BigDecimal validity = new BigDecimal("365"); + + @BeforeEach + void setUp() { + // Create test entity + entity = new ProductConsumer(); + ProductConsumerId id = new ProductConsumerId(); + id.setConsumerId(consumerId); + id.setProductId(dataProviderId); + entity.setId(id); + entity.setGrantedTs(grantedTs); + entity.setValidity(validity); + + // Create test DTO + dto = new ProductConsumerDTO(); + dto.setConsumerId(consumerId); + dto.setProductId(dataProviderId); + dto.setGrantedTs(grantedTs); + dto.setValidity(validity); + } + + @Test + void toDto_withNullEntity_shouldReturnNull() { + // Act + ProductConsumerDTO result = converter.toDto(null); + + // Assert + assertNull(result); + } + + @Test + void toDto_withValidEntity_shouldReturnCorrectDTO() { + // Act + ProductConsumerDTO result = converter.toDto(entity); + + // Assert + assertNotNull(result); + assertEquals(consumerId, result.getConsumerId()); + assertEquals(dataProviderId, result.getProductId()); + assertEquals(grantedTs, result.getGrantedTs()); + assertEquals(validity, result.getValidity()); + } + + @Test + void toEntity_withNullDTO_shouldReturnNull() { + // Act + ProductConsumer result = converter.toEntity(null); + + // Assert + assertNull(result); + } + + @Test + void toEntity_withValidDTO_shouldReturnCorrectEntity() { + // Act + ProductConsumer result = converter.toEntity(dto); + + // Assert + assertNotNull(result); + assertNotNull(result.getId()); + assertEquals(consumerId, result.getId().getConsumerId()); + assertEquals(dataProviderId, result.getId().getProductId()); + assertEquals(grantedTs, result.getGrantedTs()); + assertEquals(validity, result.getValidity()); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProductConverterTest.java b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProductConverterTest.java new file mode 100644 index 0000000..b773793 --- /dev/null +++ b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/converter/impl/ProductConverterTest.java @@ -0,0 +1,165 @@ +package uk.gov.dbt.ndtp.ia.node.management.converter.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProductDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Product; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Producer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.ProducerRepository; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ProductConverterTest { + + @Mock + private ProducerRepository producerRepository; + + @InjectMocks + private ProductConverter converter; + + private Product entity; + private ProductDTO dto; + private Producer producer; + + private final Long dataProviderId = 1L; + private final String dataProviderName = "Test Data Provider"; + private final String topic = "test-topic"; + private final Long producerId = 101L; + private final String producerName = "Test Producer"; + + @BeforeEach + void setUp() { + // Create test producer + producer = new Producer(); + producer.setId(producerId); + producer.setName(producerName); + + // Create test entity + entity = new Product(); + entity.setId(dataProviderId); + entity.setName(dataProviderName); + entity.setTopic(topic); + entity.setProducer(producer); + + // Create test DTO + dto = new ProductDTO(); + dto.setId(dataProviderId); + dto.setName(dataProviderName); + dto.setTopic(topic); + dto.setProducerId(producerId); + } + + @Test + void toDto_withNullEntity_shouldReturnNull() { + // Act + ProductDTO result = converter.toDto(null); + + // Assert + assertNull(result); + } + + @Test + void toDto_withValidEntity_shouldReturnCorrectDTO() { + // Act + ProductDTO result = converter.toDto(entity); + + // Assert + assertNotNull(result); + assertEquals(dataProviderId, result.getId()); + assertEquals(dataProviderName, result.getName()); + assertEquals(topic, result.getTopic()); + assertEquals(producerId, result.getProducerId()); + } + + @Test + void toDto_withNullProducer_shouldReturnDTOWithNullProducerId() { + // Arrange + entity.setProducer(null); + + // Act + ProductDTO result = converter.toDto(entity); + + // Assert + assertNotNull(result); + assertEquals(dataProviderId, result.getId()); + assertEquals(dataProviderName, result.getName()); + assertEquals(topic, result.getTopic()); + assertNull(result.getProducerId()); + } + + @Test + void toEntity_withNullDTO_shouldReturnNull() { + // Act + Product result = converter.toEntity(null); + + // Assert + assertNull(result); + } + + @Test + void toEntity_withValidDTO_shouldReturnCorrectEntity() { + // Arrange + when(producerRepository.findById(producerId)).thenReturn(Optional.of(producer)); + + // Act + Product result = converter.toEntity(dto); + + // Assert + assertNotNull(result); + assertEquals(dataProviderId, result.getId()); + assertEquals(dataProviderName, result.getName()); + assertEquals(topic, result.getTopic()); + assertNotNull(result.getProducer()); + assertEquals(producerId, result.getProducer().getId()); + assertEquals(producerName, result.getProducer().getName()); + + // Verify + verify(producerRepository, times(1)).findById(producerId); + } + + @Test + void toEntity_withNullProducerId_shouldReturnEntityWithNullProducer() { + // Arrange + dto.setProducerId(null); + + // Act + Product result = converter.toEntity(dto); + + // Assert + assertNotNull(result); + assertEquals(dataProviderId, result.getId()); + assertEquals(dataProviderName, result.getName()); + assertEquals(topic, result.getTopic()); + assertNull(result.getProducer()); + + // Verify + verify(producerRepository, never()).findById(any()); + } + + @Test + void toEntity_withNonExistentProducerId_shouldReturnEntityWithNullProducer() { + // Arrange + when(producerRepository.findById(producerId)).thenReturn(Optional.empty()); + + // Act + Product result = converter.toEntity(dto); + + // Assert + assertNotNull(result); + assertEquals(dataProviderId, result.getId()); + assertEquals(dataProviderName, result.getName()); + assertEquals(topic, result.getTopic()); + assertNull(result.getProducer()); + + // Verify + verify(producerRepository, times(1)).findById(producerId); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/dbt/ndtp/ia/node/management/exception/AuthenticationProcessingExceptionTest.java b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/exception/AuthenticationProcessingExceptionTest.java new file mode 100644 index 0000000..fbc645e --- /dev/null +++ b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/exception/AuthenticationProcessingExceptionTest.java @@ -0,0 +1,34 @@ +package uk.gov.dbt.ndtp.ia.node.management.exception; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class AuthenticationProcessingExceptionTest { + + private static final String CLIENT_ID = "test-client"; + private static final String MESSAGE = "Test exception message"; + private static final Exception CAUSE = new RuntimeException("Test cause"); + + @Test + void constructor_withMessageAndClientId_shouldIncludeClientIdInMessage() { + // Act + AuthenticationProcessingException exception = new AuthenticationProcessingException(MESSAGE, CLIENT_ID); + + // Assert + assertTrue(exception.getMessage().contains(MESSAGE)); + assertTrue(exception.getMessage().contains(CLIENT_ID)); + assertEquals(CLIENT_ID, exception.getClientId()); + } + + @Test + void constructor_withMessageCauseAndClientId_shouldIncludeClientIdInMessage() { + // Act + AuthenticationProcessingException exception = new AuthenticationProcessingException(MESSAGE, CAUSE, CLIENT_ID); + + // Assert + assertTrue(exception.getMessage().contains(MESSAGE)); + assertTrue(exception.getMessage().contains(CLIENT_ID)); + assertEquals(CLIENT_ID, exception.getClientId()); + assertEquals(CAUSE, exception.getCause()); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/dbt/ndtp/ia/node/management/exception/SpecificExceptionsTest.java b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/exception/SpecificExceptionsTest.java new file mode 100644 index 0000000..c46f7f5 --- /dev/null +++ b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/exception/SpecificExceptionsTest.java @@ -0,0 +1,89 @@ +package uk.gov.dbt.ndtp.ia.node.management.exception; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for the specific exception types that extend AuthenticationProcessingException. + */ +class SpecificExceptionsTest { + + private static final String CLIENT_ID = "test-client"; + private static final String MESSAGE = "Test exception message"; + private static final Exception CAUSE = new RuntimeException("Test cause"); + + @Test + void resourceAccessParsingException_withMessageAndClientId_shouldIncludeClientIdInMessage() { + // Act + ResourceAccessParsingException exception = new ResourceAccessParsingException(MESSAGE, CLIENT_ID); + + // Assert + assertTrue(exception.getMessage().contains(MESSAGE)); + assertTrue(exception.getMessage().contains(CLIENT_ID)); + assertEquals(CLIENT_ID, exception.getClientId()); + assertTrue(exception instanceof AuthenticationProcessingException); + } + + @Test + void resourceAccessParsingException_withMessageCauseAndClientId_shouldIncludeClientIdInMessage() { + // Act + ResourceAccessParsingException exception = new ResourceAccessParsingException(MESSAGE, CAUSE, CLIENT_ID); + + // Assert + assertTrue(exception.getMessage().contains(MESSAGE)); + assertTrue(exception.getMessage().contains(CLIENT_ID)); + assertEquals(CLIENT_ID, exception.getClientId()); + assertEquals(CAUSE, exception.getCause()); + assertTrue(exception instanceof AuthenticationProcessingException); + } + + @Test + void jwtClaimParsingException_withMessageAndClientId_shouldIncludeClientIdInMessage() { + // Act + JwtClaimParsingException exception = new JwtClaimParsingException(MESSAGE, CLIENT_ID); + + // Assert + assertTrue(exception.getMessage().contains(MESSAGE)); + assertTrue(exception.getMessage().contains(CLIENT_ID)); + assertEquals(CLIENT_ID, exception.getClientId()); + assertTrue(exception instanceof AuthenticationProcessingException); + } + + @Test + void jwtClaimParsingException_withMessageCauseAndClientId_shouldIncludeClientIdInMessage() { + // Act + JwtClaimParsingException exception = new JwtClaimParsingException(MESSAGE, CAUSE, CLIENT_ID); + + // Assert + assertTrue(exception.getMessage().contains(MESSAGE)); + assertTrue(exception.getMessage().contains(CLIENT_ID)); + assertEquals(CLIENT_ID, exception.getClientId()); + assertEquals(CAUSE, exception.getCause()); + assertTrue(exception instanceof AuthenticationProcessingException); + } + + @Test + void tokenIntrospectionException_withMessageAndClientId_shouldIncludeClientIdInMessage() { + // Act + TokenIntrospectionException exception = new TokenIntrospectionException(MESSAGE, CLIENT_ID); + + // Assert + assertTrue(exception.getMessage().contains(MESSAGE)); + assertTrue(exception.getMessage().contains(CLIENT_ID)); + assertEquals(CLIENT_ID, exception.getClientId()); + assertTrue(exception instanceof AuthenticationProcessingException); + } + + @Test + void tokenIntrospectionException_withMessageCauseAndClientId_shouldIncludeClientIdInMessage() { + // Act + TokenIntrospectionException exception = new TokenIntrospectionException(MESSAGE, CAUSE, CLIENT_ID); + + // Assert + assertTrue(exception.getMessage().contains(MESSAGE)); + assertTrue(exception.getMessage().contains(CLIENT_ID)); + assertEquals(CLIENT_ID, exception.getClientId()); + assertEquals(CAUSE, exception.getCause()); + assertTrue(exception instanceof AuthenticationProcessingException); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/dbt/ndtp/ia/node/management/exception/handlers/GlobalExceptionHandlerTest.java b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/exception/handlers/GlobalExceptionHandlerTest.java new file mode 100644 index 0000000..2982b82 --- /dev/null +++ b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/exception/handlers/GlobalExceptionHandlerTest.java @@ -0,0 +1,107 @@ +package uk.gov.dbt.ndtp.ia.node.management.exception.handlers; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.context.request.WebRequest; +import uk.gov.dbt.ndtp.ia.node.management.exception.AuthenticationProcessingException; +import uk.gov.dbt.ndtp.ia.node.management.exception.ErrorResponse; +import uk.gov.dbt.ndtp.ia.node.management.exception.JwtClaimParsingException; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for the GlobalExceptionHandler class. + * Verifies that each exception handler method returns the correct HTTP status code + * and ErrorResponse object with appropriate values. + */ +class GlobalExceptionHandlerTest { + + private GlobalExceptionHandler exceptionHandler; + + @Mock + private WebRequest webRequest; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + exceptionHandler = new GlobalExceptionHandler(); + } + + @Test + void handleAuthenticationProcessingException_shouldReturnUnauthorizedStatus() { + // Arrange + String clientId = "test-client"; + String message = "Authentication failed"; + AuthenticationProcessingException exception = new AuthenticationProcessingException(message, clientId); + + // Act + ResponseEntity response = exceptionHandler.handleAuthenticationProcessingException(exception, webRequest); + + // Assert + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + ErrorResponse errorResponse = response.getBody(); + assertNotNull(errorResponse); + assertEquals(HttpStatus.UNAUTHORIZED.value(), errorResponse.getStatus()); + assertTrue(errorResponse.getMessage().contains(message)); + assertNotNull(errorResponse.getErrorId()); + } + + @Test + void handleAuthenticationProcessingException_withSubclass_shouldReturnUnauthorizedStatus() { + // Arrange + String clientId = "test-client"; + String message = "JWT claim parsing failed"; + JwtClaimParsingException exception = new JwtClaimParsingException(message, clientId); + + // Act + ResponseEntity response = exceptionHandler.handleAuthenticationProcessingException(exception, webRequest); + + // Assert + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + ErrorResponse errorResponse = response.getBody(); + assertNotNull(errorResponse); + assertEquals(HttpStatus.UNAUTHORIZED.value(), errorResponse.getStatus()); + assertTrue(errorResponse.getMessage().contains(message)); + assertNotNull(errorResponse.getErrorId()); + } + + @Test + void handleRuntimeException_shouldReturnInternalServerErrorStatus() { + // Arrange + String message = "Something went wrong"; + RuntimeException exception = new RuntimeException(message); + + // Act + ResponseEntity response = exceptionHandler.handleRuntimeException(exception, webRequest); + + // Assert + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + ErrorResponse errorResponse = response.getBody(); + assertNotNull(errorResponse); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR.value(), errorResponse.getStatus()); + assertEquals("An internal server error occurred", errorResponse.getMessage()); + assertNotNull(errorResponse.getErrorId()); + } + + @Test + void handleAllExceptions_shouldReturnInternalServerErrorStatus() { + // Arrange + String message = "Generic exception"; + Exception exception = new Exception(message); + + // Act + ResponseEntity response = exceptionHandler.handleAllExceptions(exception, webRequest); + + // Assert + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + ErrorResponse errorResponse = response.getBody(); + assertNotNull(errorResponse); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR.value(), errorResponse.getStatus()); + assertEquals("An unexpected error occurred", errorResponse.getMessage()); + assertNotNull(errorResponse.getErrorId()); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ConsumerProviderOrganisationServiceImplTest.java b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ConsumerProviderOrganisationServiceImplTest.java new file mode 100644 index 0000000..08f4fe8 --- /dev/null +++ b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ConsumerProviderOrganisationServiceImplTest.java @@ -0,0 +1,100 @@ +package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.dbt.ndtp.ia.node.management.converter.impl.ProductConsumerConverter; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProductConsumerDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumerId; +import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.ConsumerProviderRepository; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ConsumerProviderOrganisationServiceImplTest { + + @Mock + private ConsumerProviderRepository consumerProviderRepository; + + @Mock + private ProductConsumerConverter productConsumerConverter; + + @InjectMocks + private ProductConsumerServiceImpl consumerAllowedDataProviderService; + + private ProductConsumer entity1; + private ProductConsumer entity2; + private ProductConsumerDTO dto1; + private ProductConsumerDTO dto2; + private final Long consumerId = 1L; + + @BeforeEach + void setUp() { + // Create test entities + entity1 = new ProductConsumer(); + ProductConsumerId id1 = new ProductConsumerId(); + id1.setConsumerId(consumerId); + id1.setProductId(101L); + entity1.setId(id1); + entity1.setGrantedTs(Timestamp.from(Instant.now())); + entity1.setValidity(new BigDecimal("365")); + + entity2 = new ProductConsumer(); + ProductConsumerId id2 = new ProductConsumerId(); + id2.setConsumerId(consumerId); + id2.setProductId(102L); + entity2.setId(id2); + entity2.setGrantedTs(Timestamp.from(Instant.now())); + entity2.setValidity(new BigDecimal("180")); + + // Create test DTOs + dto1 = new ProductConsumerDTO(); + dto1.setConsumerId(consumerId); + dto1.setProductId(101L); + dto1.setGrantedTs(entity1.getGrantedTs()); + dto1.setValidity(entity1.getValidity()); + + dto2 = new ProductConsumerDTO(); + dto2.setConsumerId(consumerId); + dto2.setProductId(102L); + dto2.setGrantedTs(entity2.getGrantedTs()); + dto2.setValidity(entity2.getValidity()); + } + + @Test + void findByConsumerId_shouldReturnDTOList() { + // Arrange + List entities = Arrays.asList(entity1, entity2); + List dtos = Arrays.asList(dto1, dto2); + when(consumerProviderRepository.findByConsumerId(consumerId)).thenReturn(entities); + when(productConsumerConverter.toDtoList(entities)).thenReturn(dtos); + + // Act + List result = consumerAllowedDataProviderService.findByConsumerId(consumerId); + + // Assert + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals(dto1.getConsumerId(), result.get(0).getConsumerId()); + assertEquals(dto1.getProductId(), result.get(0).getProductId()); + assertEquals(dto1.getGrantedTs(), result.get(0).getGrantedTs()); + assertEquals(dto1.getValidity(), result.get(0).getValidity()); + + assertEquals(dto2.getConsumerId(), result.get(1).getConsumerId()); + assertEquals(dto2.getProductId(), result.get(1).getProductId()); + assertEquals(dto2.getGrantedTs(), result.get(1).getGrantedTs()); + assertEquals(dto2.getValidity(), result.get(1).getValidity()); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ConsumerServiceImplTest.java b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ConsumerServiceImplTest.java new file mode 100644 index 0000000..8a03261 --- /dev/null +++ b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ConsumerServiceImplTest.java @@ -0,0 +1,172 @@ +package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.dbt.ndtp.ia.node.management.converter.impl.ConsumerConverter; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ConsumerDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Consumer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.ConsumerRepository; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ConsumerServiceImplTest { + + @Mock + private ConsumerRepository consumerRepository; + + @Mock + private ConsumerConverter consumerConverter; + + @InjectMocks + private ConsumerServiceImpl consumerService; + + private Consumer consumer; + private ConsumerDTO consumerDTO; + private final Long consumerId = 1L; + private final String idpClientId = "test-client-id"; + + @BeforeEach + void setUp() { + // Set up test data + consumer = new Consumer(); + consumer.setId(consumerId); + consumer.setIdpClientId(idpClientId); + consumer.setName("Test Consumer"); + + consumerDTO = ConsumerDTO.builder() + .id(consumerId) + .idpClientId(idpClientId) + .name("Test Consumer") + .build(); + } + + @Test + void findById_withExistingId_shouldReturnConsumerDTO() { + // Arrange + when(consumerRepository.findById(consumerId)).thenReturn(Optional.of(consumer)); + when(consumerConverter.toDto(consumer)).thenReturn(consumerDTO); + + // Act + Optional result = consumerService.findById(consumerId); + + // Assert + assertTrue(result.isPresent()); + assertEquals(consumerId, result.get().getId()); + assertEquals(idpClientId, result.get().getIdpClientId()); + assertEquals("Test Consumer", result.get().getName()); + + // Verify + verify(consumerRepository).findById(consumerId); + verify(consumerConverter).toDto(consumer); + } + + @Test + void findById_withNonExistingId_shouldReturnEmptyOptional() { + // Arrange + when(consumerRepository.findById(consumerId)).thenReturn(Optional.empty()); + + // Act + Optional result = consumerService.findById(consumerId); + + // Assert + assertFalse(result.isPresent()); + + // Verify + verify(consumerRepository).findById(consumerId); + verify(consumerConverter, never()).toDto(any()); + } + + @Test + void findByIdpClientId_withExistingClientId_shouldReturnListOfConsumerDTOs() { + // Arrange + List consumers = List.of(consumer); + List consumerDTOs = List.of(consumerDTO); + + when(consumerRepository.findByIdpClientId(idpClientId)).thenReturn(consumers); + when(consumerConverter.toDtoList(consumers)).thenReturn(consumerDTOs); + + // Act + List result = consumerService.findByIdpClientId(idpClientId); + + // Assert + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals(consumerId, result.get(0).getId()); + assertEquals(idpClientId, result.get(0).getIdpClientId()); + + // Verify + verify(consumerRepository).findByIdpClientId(idpClientId); + verify(consumerConverter).toDtoList(consumers); + } + + @Test + void findByIdpClientId_withNonExistingClientId_shouldReturnEmptyList() { + // Arrange + when(consumerRepository.findByIdpClientId(idpClientId)).thenReturn(Collections.emptyList()); + when(consumerConverter.toDtoList(Collections.emptyList())).thenReturn(Collections.emptyList()); + + // Act + List result = consumerService.findByIdpClientId(idpClientId); + + // Assert + assertNotNull(result); + assertTrue(result.isEmpty()); + + // Verify + verify(consumerRepository).findByIdpClientId(idpClientId); + verify(consumerConverter).toDtoList(Collections.emptyList()); + } + + @Test + void getConsumersOfProviders_withValidProviderIds_shouldReturnMappedConsumers() { + // Arrange + List providerIds = List.of(1L, 2L); + List consumers = List.of(consumer); + List consumerDTOs = List.of(consumerDTO); + + when(consumerRepository.findConsumersByProviderIds(providerIds)).thenReturn(consumers); + when(consumerConverter.toDto(consumer)).thenReturn(consumerDTO); + + // Act + Map> result = consumerService.getConsumersOfProviders(providerIds); + + // Assert + assertNotNull(result); + assertEquals(1, result.size()); + assertTrue(result.containsKey(idpClientId)); + assertEquals(1, result.get(idpClientId).size()); + assertEquals(consumerId, result.get(idpClientId).get(0).getId()); + + // Verify + verify(consumerRepository).findConsumersByProviderIds(providerIds); + verify(consumerConverter).toDto(consumer); + } + + @Test + void getConsumersOfProviders_withEmptyProviderIds_shouldReturnEmptyMap() { + // Arrange + List emptyProviderIds = Collections.emptyList(); + when(consumerRepository.findConsumersByProviderIds(emptyProviderIds)).thenReturn(Collections.emptyList()); + + // Act + Map> result = consumerService.getConsumersOfProviders(emptyProviderIds); + + // Assert + assertNotNull(result); + assertTrue(result.isEmpty()); + + // Verify + verify(consumerRepository).findConsumersByProviderIds(emptyProviderIds); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/OrganisationServiceImplTest.java b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/OrganisationServiceImplTest.java new file mode 100644 index 0000000..9fd1577 --- /dev/null +++ b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/OrganisationServiceImplTest.java @@ -0,0 +1,33 @@ +package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.OrganisationRepository; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@ExtendWith(MockitoExtension.class) +class OrganisationServiceImplTest { + + @Mock + private OrganisationRepository organisationRepository; + + @InjectMocks + private OrganisationServiceImpl organisationService; + + @BeforeEach + void setUp() { + // No setup needed as the service has no methods to test yet + } + + @Test + void organisationService_shouldBeInitialized() { + // This test verifies that the service is properly initialized with its dependencies + assertNotNull(organisationService); + assertNotNull(organisationRepository); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProducerServiceImplTest.java b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProducerServiceImplTest.java new file mode 100644 index 0000000..609a64e --- /dev/null +++ b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProducerServiceImplTest.java @@ -0,0 +1,148 @@ +package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.dbt.ndtp.ia.node.management.converter.impl.OrganisationProducerConverter; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProducerDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Producer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.ProducerRepository; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ProducerServiceImplTest { + + @Mock + private ProducerRepository producerRepository; + + @Mock + private OrganisationProducerConverter organisationProducerConverter; + + @InjectMocks + private ProducerServiceImpl producerService; + + private Producer producer; + private ProducerDTO producerDTO; + private final Long producerId = 1L; + private final String clientId = "test-client-id"; + + @BeforeEach + void setUp() { + // Set up test data + producer = new Producer(); + producer.setId(producerId); + producer.setIdpClientId(clientId); + producer.setName("Test Producer"); + producer.setActive(true); + + producerDTO = ProducerDTO.builder() + .id(producerId) + .idpClientId(clientId) + .name("Test Producer") + .active(true) + .build(); + } + + @Test + void getProducersByIds_withValidIds_shouldReturnProducerDTOs() { + // Arrange + List producerIds = List.of(producerId); + List producers = List.of(producer); + List producerDTOs = List.of(producerDTO); + + when(producerRepository.findByIds(producerIds)).thenReturn(producers); + when(organisationProducerConverter.toDtoList(producers)).thenReturn(producerDTOs); + + // Act + List result = producerService.getProducersByIds(producerIds); + + // Assert + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals(producerId, result.get(0).getId()); + assertEquals(clientId, result.get(0).getIdpClientId()); + assertEquals("Test Producer", result.get(0).getName()); + assertEquals(true, result.get(0).getActive()); + + // Verify + verify(producerRepository).findByIds(producerIds); + verify(organisationProducerConverter).toDtoList(producers); + } + + @Test + void getProducersByIds_withEmptyIds_shouldReturnEmptyList() { + // Arrange + List emptyIds = Collections.emptyList(); + List emptyProducers = Collections.emptyList(); + List emptyDTOs = Collections.emptyList(); + + when(producerRepository.findByIds(emptyIds)).thenReturn(emptyProducers); + when(organisationProducerConverter.toDtoList(emptyProducers)).thenReturn(emptyDTOs); + + // Act + List result = producerService.getProducersByIds(emptyIds); + + // Assert + assertNotNull(result); + assertTrue(result.isEmpty()); + + // Verify + verify(producerRepository).findByIds(emptyIds); + verify(organisationProducerConverter).toDtoList(emptyProducers); + } + + @Test + void getProducersByClientId_withValidClientId_shouldReturnProducerDTOs() { + // Arrange + List producers = List.of(producer); + List producerDTOs = List.of(producerDTO); + + when(producerRepository.findByIdpClientId(clientId)).thenReturn(producers); + when(organisationProducerConverter.toDtoList(producers)).thenReturn(producerDTOs); + + // Act + List result = producerService.getProducersByClientId(clientId); + + // Assert + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals(producerId, result.get(0).getId()); + assertEquals(clientId, result.get(0).getIdpClientId()); + assertEquals("Test Producer", result.get(0).getName()); + assertEquals(true, result.get(0).getActive()); + + // Verify + verify(producerRepository).findByIdpClientId(clientId); + verify(organisationProducerConverter).toDtoList(producers); + } + + @Test + void getProducersByClientId_withNonExistingClientId_shouldReturnEmptyList() { + // Arrange + String nonExistingClientId = "non-existing-client-id"; + List emptyProducers = Collections.emptyList(); + List emptyDTOs = Collections.emptyList(); + + when(producerRepository.findByIdpClientId(nonExistingClientId)).thenReturn(emptyProducers); + when(organisationProducerConverter.toDtoList(emptyProducers)).thenReturn(emptyDTOs); + + // Act + List result = producerService.getProducersByClientId(nonExistingClientId); + + // Assert + assertNotNull(result); + assertTrue(result.isEmpty()); + + // Verify + verify(producerRepository).findByIdpClientId(nonExistingClientId); + verify(organisationProducerConverter).toDtoList(emptyProducers); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProductConsumerServiceImplTest.java b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProductConsumerServiceImplTest.java new file mode 100644 index 0000000..e3bc03e --- /dev/null +++ b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProductConsumerServiceImplTest.java @@ -0,0 +1,150 @@ +package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.dbt.ndtp.ia.node.management.converter.impl.ProductConsumerConverter; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProductConsumerDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumerId; +import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.ConsumerProviderRepository; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ProductConsumerServiceImplTest { + + @Mock + private ConsumerProviderRepository consumerProviderRepository; + + @Mock + private ProductConsumerConverter productConsumerConverter; + + @InjectMocks + private ProductConsumerServiceImpl productConsumerService; + + private ProductConsumer productConsumer; + private ProductConsumerDTO productConsumerDTO; + private final Long consumerId = 1L; + private final Long productId = 2L; + + @BeforeEach + void setUp() { + // Set up test data + ProductConsumerId id = new ProductConsumerId(); + id.setConsumerId(consumerId); + id.setProductId(productId); + + productConsumer = new ProductConsumer(); + productConsumer.setId(id); + productConsumer.setGrantedTs(Timestamp.from(Instant.now())); + productConsumer.setValidity(BigDecimal.ZERO); + + productConsumerDTO = ProductConsumerDTO.builder() + .consumerId(consumerId) + .productId(productId) + .grantedTs(Timestamp.from(Instant.now())) + .validity(BigDecimal.ZERO) + .build(); + } + + @Test + void findByConsumerId_withValidId_shouldReturnProductConsumerDTOs() { + // Arrange + List productConsumers = List.of(productConsumer); + List productConsumerDTOs = List.of(productConsumerDTO); + + when(consumerProviderRepository.findByConsumerId(consumerId)).thenReturn(productConsumers); + when(productConsumerConverter.toDtoList(productConsumers)).thenReturn(productConsumerDTOs); + + // Act + List result = productConsumerService.findByConsumerId(consumerId); + + // Assert + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals(consumerId, result.get(0).getConsumerId()); + assertEquals(productId, result.get(0).getProductId()); + + // Verify + verify(consumerProviderRepository).findByConsumerId(consumerId); + verify(productConsumerConverter).toDtoList(productConsumers); + } + + @Test + void findByConsumerId_withNonExistingId_shouldReturnEmptyList() { + // Arrange + Long nonExistingId = 999L; + List emptyList = Collections.emptyList(); + List emptyDTOList = Collections.emptyList(); + + when(consumerProviderRepository.findByConsumerId(nonExistingId)).thenReturn(emptyList); + when(productConsumerConverter.toDtoList(emptyList)).thenReturn(emptyDTOList); + + // Act + List result = productConsumerService.findByConsumerId(nonExistingId); + + // Assert + assertNotNull(result); + assertTrue(result.isEmpty()); + + // Verify + verify(consumerProviderRepository).findByConsumerId(nonExistingId); + verify(productConsumerConverter).toDtoList(emptyList); + } + + @Test + void findByDataProviderId_withValidId_shouldReturnProductConsumerDTOs() { + // Arrange + List productConsumers = List.of(productConsumer); + List productConsumerDTOs = List.of(productConsumerDTO); + + when(consumerProviderRepository.findByProductId(productId)).thenReturn(productConsumers); + when(productConsumerConverter.toDtoList(productConsumers)).thenReturn(productConsumerDTOs); + + // Act + List result = productConsumerService.findByDataProviderId(productId); + + // Assert + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals(consumerId, result.get(0).getConsumerId()); + assertEquals(productId, result.get(0).getProductId()); + + // Verify + verify(consumerProviderRepository).findByProductId(productId); + verify(productConsumerConverter).toDtoList(productConsumers); + } + + @Test + void findByDataProviderId_withNonExistingId_shouldReturnEmptyList() { + // Arrange + Long nonExistingId = 999L; + List emptyList = Collections.emptyList(); + List emptyDTOList = Collections.emptyList(); + + when(consumerProviderRepository.findByProductId(nonExistingId)).thenReturn(emptyList); + when(productConsumerConverter.toDtoList(emptyList)).thenReturn(emptyDTOList); + + // Act + List result = productConsumerService.findByDataProviderId(nonExistingId); + + // Assert + assertNotNull(result); + assertTrue(result.isEmpty()); + + // Verify + verify(consumerProviderRepository).findByProductId(nonExistingId); + verify(productConsumerConverter).toDtoList(emptyList); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProductServiceImplTest.java b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProductServiceImplTest.java new file mode 100644 index 0000000..835b2bd --- /dev/null +++ b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/data/impl/ProductServiceImplTest.java @@ -0,0 +1,195 @@ +package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.dbt.ndtp.ia.node.management.converter.impl.ProductConverter; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProductDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Producer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Product; +import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.ProductRepository; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ProductServiceImplTest { + + @Mock + private ProductRepository productRepository; + + @Mock + private ProductConverter productConverter; + + @InjectMocks + private ProductServiceImpl productService; + + private Product product; + private ProductDTO productDTO; + private final Long productId = 1L; + private final Long producerId = 2L; + private final String productName = "Test Product"; + + @BeforeEach + void setUp() { + // Set up test data + Producer producer = new Producer(); + producer.setId(producerId); + producer.setName("Test Producer"); + + product = new Product(); + product.setId(productId); + product.setName(productName); + product.setTopic("test-topic"); + product.setProducer(producer); + + productDTO = ProductDTO.builder() + .id(productId) + .name(productName) + .producerId(producerId) + .build(); + } + + @Test + void getProductsByIds_withValidIds_shouldReturnProductDTOs() { + // Arrange + List productIds = List.of(productId); + List products = List.of(product); + List productDTOs = List.of(productDTO); + + when(productRepository.findByIds(productIds)).thenReturn(products); + when(productConverter.toDtoList(products)).thenReturn(productDTOs); + + // Act + List result = productService.getProductsByIds(productIds); + + // Assert + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals(productId, result.get(0).getId()); + assertEquals(productName, result.get(0).getName()); + assertEquals(producerId, result.get(0).getProducerId()); + + // Verify + verify(productRepository).findByIds(productIds); + verify(productConverter).toDtoList(products); + } + + @Test + void getProductsByIds_withEmptyIds_shouldReturnEmptyList() { + // Arrange + List emptyIds = Collections.emptyList(); + + // Act + List result = productService.getProductsByIds(emptyIds); + + // Assert + assertNotNull(result); + assertTrue(result.isEmpty()); + + // Verify + verify(productRepository, never()).findByIds(any()); + } + + @Test + void getProductsByIds_withNullIds_shouldReturnEmptyList() { + // Act + List result = productService.getProductsByIds(null); + + // Assert + assertNotNull(result); + assertTrue(result.isEmpty()); + + // Verify + verify(productRepository, never()).findByIds(any()); + } + + @Test + void getProductsByIds_withNullRepositoryResult_shouldReturnEmptyList() { + // Arrange + List productIds = List.of(productId); + when(productRepository.findByIds(productIds)).thenReturn(null); + + // Act + List result = productService.getProductsByIds(productIds); + + // Assert + assertNotNull(result); + assertTrue(result.isEmpty()); + + // Verify + verify(productRepository).findByIds(productIds); + verify(productConverter, never()).toDtoList(any()); + } + + @Test + void getProductsByProducerIds_withValidIds_shouldReturnProductDTOs() { + // Arrange + List producerIds = List.of(producerId); + List products = List.of(product); + List productDTOs = List.of(productDTO); + + when(productRepository.findByProducerIds(producerIds)).thenReturn(products); + when(productConverter.toDtoList(products)).thenReturn(productDTOs); + + // Act + List result = productService.getProductsByProducerIds(producerIds); + + // Assert + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals(productId, result.get(0).getId()); + assertEquals(productName, result.get(0).getName()); + assertEquals(producerId, result.get(0).getProducerId()); + + // Verify + verify(productRepository).findByProducerIds(producerIds); + verify(productConverter).toDtoList(products); + } + + @Test + void getProductsByProducerIds_withEmptyIds_shouldReturnEmptyList() { + // Arrange + List emptyIds = Collections.emptyList(); + List emptyProducts = Collections.emptyList(); + List emptyDTOs = Collections.emptyList(); + + when(productRepository.findByProducerIds(emptyIds)).thenReturn(emptyProducts); + when(productConverter.toDtoList(emptyProducts)).thenReturn(emptyDTOs); + + // Act + List result = productService.getProductsByProducerIds(emptyIds); + + // Assert + assertNotNull(result); + assertTrue(result.isEmpty()); + + // Verify + verify(productRepository).findByProducerIds(emptyIds); + verify(productConverter).toDtoList(emptyProducts); + } + + @Test + void getProductsByProducerIds_withNullRepositoryResult_shouldReturnEmptyList() { + // Arrange + List producerIds = List.of(producerId); + when(productRepository.findByProducerIds(producerIds)).thenReturn(null); + + // Act + List result = productService.getProductsByProducerIds(producerIds); + + // Assert + assertNotNull(result); + assertTrue(result.isEmpty()); + + // Verify + verify(productRepository).findByProducerIds(producerIds); + verify(productConverter, never()).toDtoList(any()); + } +} \ No newline at end of file diff --git a/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/providers/configuration/ConfigurationProviderImplTest.java b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/providers/configuration/ConfigurationProviderImplTest.java new file mode 100644 index 0000000..4c427c6 --- /dev/null +++ b/src/test/java/uk/gov/dbt/ndtp/ia/node/management/service/providers/configuration/ConfigurationProviderImplTest.java @@ -0,0 +1,714 @@ +package uk.gov.dbt.ndtp.ia.node.management.service.providers.configuration; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.*; +import uk.gov.dbt.ndtp.ia.node.management.service.data.ConsumerService; +import uk.gov.dbt.ndtp.ia.node.management.service.data.ProductConsumerService; +import uk.gov.dbt.ndtp.ia.node.management.service.data.ProductService; +import uk.gov.dbt.ndtp.ia.node.management.service.data.ProducerService; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ConfigurationProviderImplTest { + + @Mock + private ConsumerService consumerService; + + @Mock + private ProductConsumerService consumerAllowedDataProvidersService; + + @Mock + private ProductService dataProviderService; + + @Mock + private ProducerService producerService; + + @InjectMocks + private ConfigurationProviderImpl configurationProvider; + + private final String clientId = "test-client-id"; + private final Long consumerId = 1L; + private final Long producerId = 2L; + private final Long productId = 3L; + + private ConsumerDTO consumerDTO; + private ProducerDTO producerDTO; + private ProductDTO productDTO; + private ProductConsumerDTO productConsumerDTO; + + @BeforeEach + void setUp() { + // Set up consumer + consumerDTO = ConsumerDTO.builder() + .id(consumerId) + .name("Test Consumer") + .idpClientId(clientId) + .build(); + + // Set up producer + producerDTO = ProducerDTO.builder() + .id(producerId) + .name("Test Producer") + .idpClientId(clientId) + .active(true) + .build(); + + // Set up product + productDTO = ProductDTO.builder() + .id(productId) + .name("Test Product") + .producerId(producerId) + .consumers(new ArrayList<>()) + .build(); + + // Set up product consumer relationship + productConsumerDTO = ProductConsumerDTO.builder() + .consumerId(consumerId) + .productId(productId) + .validity(null) // No validity constraint + .build(); + } + + // Tests for getConsumerConfigByClientId + + @Test + void getConsumerConfigByClientId_withValidClientIdAndNoConsumerId_shouldReturnConfig() { + // Arrange + List consumers = List.of(consumerDTO); + List productConsumers = List.of(productConsumerDTO); + List products = List.of(productDTO); + List producers = List.of(producerDTO); + + when(consumerService.findByIdpClientId(clientId)).thenReturn(consumers); + when(consumerAllowedDataProvidersService.findByConsumerId(consumerId)).thenReturn(productConsumers); + when(dataProviderService.getProductsByIds(List.of(productId))).thenReturn(products); + when(producerService.getProducersByIds(List.of(producerId))).thenReturn(producers); + + // Act + ConsumerConfigDTO result = configurationProvider.getConsumerConfigByClientId(clientId, Optional.empty()); + + // Assert + assertNotNull(result); + assertEquals(clientId, result.getClientId()); + assertEquals(1, result.getProducers().size()); + assertEquals(producerId, result.getProducers().get(0).getId()); + + // Verify + verify(consumerService).findByIdpClientId(clientId); + verify(consumerAllowedDataProvidersService).findByConsumerId(consumerId); + verify(dataProviderService).getProductsByIds(List.of(productId)); + verify(producerService).getProducersByIds(List.of(producerId)); + } + + @Test + void getConsumerConfigByClientId_withValidClientIdAndConsumerId_shouldReturnFilteredConfig() { + // Arrange + List allConsumers = List.of(consumerDTO); + List productConsumers = List.of(productConsumerDTO); + List products = List.of(productDTO); + List producers = List.of(producerDTO); + + when(consumerService.findByIdpClientId(clientId)).thenReturn(allConsumers); + when(consumerAllowedDataProvidersService.findByConsumerId(consumerId)).thenReturn(productConsumers); + when(dataProviderService.getProductsByIds(List.of(productId))).thenReturn(products); + when(producerService.getProducersByIds(List.of(producerId))).thenReturn(producers); + + // Act + ConsumerConfigDTO result = configurationProvider.getConsumerConfigByClientId(clientId, Optional.of(consumerId)); + + // Assert + assertNotNull(result); + assertEquals(clientId, result.getClientId()); + assertEquals(1, result.getProducers().size()); + assertEquals(producerId, result.getProducers().get(0).getId()); + + // Verify + verify(consumerService).findByIdpClientId(clientId); + verify(consumerAllowedDataProvidersService).findByConsumerId(consumerId); + verify(dataProviderService).getProductsByIds(List.of(productId)); + verify(producerService).getProducersByIds(List.of(producerId)); + } + + @Test + void getConsumerConfigByClientId_withNoMatchingConsumers_shouldReturnEmptyConfig() { + // Arrange + when(consumerService.findByIdpClientId(clientId)).thenReturn(Collections.emptyList()); + when(dataProviderService.getProductsByIds(Collections.emptyList())).thenReturn(Collections.emptyList()); + when(producerService.getProducersByIds(Collections.emptyList())).thenReturn(Collections.emptyList()); + + // Act + ConsumerConfigDTO result = configurationProvider.getConsumerConfigByClientId(clientId, Optional.empty()); + + // Assert + assertNotNull(result); + assertEquals(clientId, result.getClientId()); + assertTrue(result.getProducers().isEmpty()); + + // Verify + verify(consumerService).findByIdpClientId(clientId); + verify(consumerAllowedDataProvidersService, never()).findByConsumerId(any()); + verify(dataProviderService).getProductsByIds(Collections.emptyList()); + verify(producerService).getProducersByIds(Collections.emptyList()); + } + + @Test + void getConsumerConfigByClientId_withNoMatchingConsumerForSpecificId_shouldReturnEmptyConfig() { + // Arrange + ConsumerDTO differentConsumer = ConsumerDTO.builder() + .id(999L) + .idpClientId(clientId) + .build(); + + when(consumerService.findByIdpClientId(clientId)).thenReturn(List.of(differentConsumer)); + when(dataProviderService.getProductsByIds(Collections.emptyList())).thenReturn(Collections.emptyList()); + when(producerService.getProducersByIds(Collections.emptyList())).thenReturn(Collections.emptyList()); + + // Act + ConsumerConfigDTO result = configurationProvider.getConsumerConfigByClientId(clientId, Optional.of(consumerId)); + + // Assert + assertNotNull(result); + assertEquals(clientId, result.getClientId()); + assertTrue(result.getProducers().isEmpty()); + + // Verify + verify(consumerService).findByIdpClientId(clientId); + verify(consumerAllowedDataProvidersService, never()).findByConsumerId(any()); + verify(dataProviderService).getProductsByIds(Collections.emptyList()); + verify(producerService).getProducersByIds(Collections.emptyList()); + } + + @Test + void getConsumerConfigByClientId_withNoValidDataProviders_shouldReturnEmptyConfig() { + // Arrange + List consumers = List.of(consumerDTO); + + when(consumerService.findByIdpClientId(clientId)).thenReturn(consumers); + when(consumerAllowedDataProvidersService.findByConsumerId(consumerId)).thenReturn(Collections.emptyList()); + when(dataProviderService.getProductsByIds(Collections.emptyList())).thenReturn(Collections.emptyList()); + when(producerService.getProducersByIds(Collections.emptyList())).thenReturn(Collections.emptyList()); + + // Act + ConsumerConfigDTO result = configurationProvider.getConsumerConfigByClientId(clientId, Optional.empty()); + + // Assert + assertNotNull(result); + assertEquals(clientId, result.getClientId()); + assertTrue(result.getProducers().isEmpty()); + + // Verify + verify(consumerService).findByIdpClientId(clientId); + verify(consumerAllowedDataProvidersService).findByConsumerId(consumerId); + verify(dataProviderService).getProductsByIds(Collections.emptyList()); + verify(producerService).getProducersByIds(Collections.emptyList()); + } + + @Test + void getConsumerConfigByClientId_withExpiredValidity_shouldReturnEmptyConfig() { + // Arrange + List consumers = List.of(consumerDTO); + + // Create expired product consumer relationship + ProductConsumerDTO expiredProductConsumer = ProductConsumerDTO.builder() + .consumerId(consumerId) + .productId(productId) + .validity(BigDecimal.valueOf(30)) // 30 days validity + .grantedTs(Timestamp.from(Instant.now().minus(60, ChronoUnit.DAYS))) // 60 days ago + .build(); + + when(consumerService.findByIdpClientId(clientId)).thenReturn(consumers); + when(consumerAllowedDataProvidersService.findByConsumerId(consumerId)).thenReturn(List.of(expiredProductConsumer)); + when(dataProviderService.getProductsByIds(Collections.emptyList())).thenReturn(Collections.emptyList()); + when(producerService.getProducersByIds(Collections.emptyList())).thenReturn(Collections.emptyList()); + + // Act + ConsumerConfigDTO result = configurationProvider.getConsumerConfigByClientId(clientId, Optional.empty()); + + // Assert + assertNotNull(result); + assertEquals(clientId, result.getClientId()); + assertTrue(result.getProducers().isEmpty()); + + // Verify + verify(consumerService).findByIdpClientId(clientId); + verify(consumerAllowedDataProvidersService).findByConsumerId(consumerId); + verify(dataProviderService).getProductsByIds(Collections.emptyList()); + verify(producerService).getProducersByIds(Collections.emptyList()); + } + + @Test + void getConsumerConfigByClientId_withValidityButNoGrantedTs_shouldReturnEmptyConfig() { + // Arrange + List consumers = List.of(consumerDTO); + + // Create product consumer relationship with validity but no grantedTs + ProductConsumerDTO invalidProductConsumer = ProductConsumerDTO.builder() + .consumerId(consumerId) + .productId(productId) + .validity(BigDecimal.valueOf(30)) // 30 days validity + .grantedTs(null) // No granted timestamp + .build(); + + when(consumerService.findByIdpClientId(clientId)).thenReturn(consumers); + when(consumerAllowedDataProvidersService.findByConsumerId(consumerId)).thenReturn(List.of(invalidProductConsumer)); + when(dataProviderService.getProductsByIds(Collections.emptyList())).thenReturn(Collections.emptyList()); + when(producerService.getProducersByIds(Collections.emptyList())).thenReturn(Collections.emptyList()); + + // Act + ConsumerConfigDTO result = configurationProvider.getConsumerConfigByClientId(clientId, Optional.empty()); + + // Assert + assertNotNull(result); + assertEquals(clientId, result.getClientId()); + assertTrue(result.getProducers().isEmpty()); + + // Verify + verify(consumerService).findByIdpClientId(clientId); + verify(consumerAllowedDataProvidersService).findByConsumerId(consumerId); + verify(dataProviderService).getProductsByIds(Collections.emptyList()); + verify(producerService).getProducersByIds(Collections.emptyList()); + } + + @Test + void getConsumerConfigByClientId_withValidityZero_shouldReturnConfig() { + // Arrange + List consumers = List.of(consumerDTO); + + // Create product consumer relationship with zero validity + ProductConsumerDTO zeroValidityProductConsumer = ProductConsumerDTO.builder() + .consumerId(consumerId) + .productId(productId) + .validity(BigDecimal.ZERO) // Zero validity means no expiration + .build(); + + List products = List.of(productDTO); + List producers = List.of(producerDTO); + + when(consumerService.findByIdpClientId(clientId)).thenReturn(consumers); + when(consumerAllowedDataProvidersService.findByConsumerId(consumerId)).thenReturn(List.of(zeroValidityProductConsumer)); + when(dataProviderService.getProductsByIds(List.of(productId))).thenReturn(products); + when(producerService.getProducersByIds(List.of(producerId))).thenReturn(producers); + + // Act + ConsumerConfigDTO result = configurationProvider.getConsumerConfigByClientId(clientId, Optional.empty()); + + // Assert + assertNotNull(result); + assertEquals(clientId, result.getClientId()); + assertEquals(1, result.getProducers().size()); + + // Verify + verify(consumerService).findByIdpClientId(clientId); + verify(consumerAllowedDataProvidersService).findByConsumerId(consumerId); + verify(dataProviderService).getProductsByIds(List.of(productId)); + verify(producerService).getProducersByIds(List.of(producerId)); + } + + @Test + void getConsumerConfigByClientId_withNoActiveProducers_shouldReturnEmptyConfig() { + // Arrange + List consumers = List.of(consumerDTO); + List productConsumers = List.of(productConsumerDTO); + List products = List.of(productDTO); + + // Create inactive producer + ProducerDTO inactiveProducer = ProducerDTO.builder() + .id(producerId) + .active(false) + .build(); + + when(consumerService.findByIdpClientId(clientId)).thenReturn(consumers); + when(consumerAllowedDataProvidersService.findByConsumerId(consumerId)).thenReturn(productConsumers); + when(dataProviderService.getProductsByIds(List.of(productId))).thenReturn(products); + when(producerService.getProducersByIds(List.of(producerId))).thenReturn(List.of(inactiveProducer)); + + // Act + ConsumerConfigDTO result = configurationProvider.getConsumerConfigByClientId(clientId, Optional.empty()); + + // Assert + assertNotNull(result); + assertEquals(clientId, result.getClientId()); + assertTrue(result.getProducers().isEmpty()); + + // Verify + verify(consumerService).findByIdpClientId(clientId); + verify(consumerAllowedDataProvidersService).findByConsumerId(consumerId); + verify(dataProviderService).getProductsByIds(List.of(productId)); + verify(producerService).getProducersByIds(List.of(producerId)); + } + + // Tests for getProducerConfigByClientId + + @Test + void getProducerConfigByClientId_withValidClientIdAndNoProducerId_shouldReturnConfig() { + // Arrange + List producers = List.of(producerDTO); + // Add product to producer's dataProviders list + producerDTO.getDataProviders().add(productDTO); + + Map> consumersMap = new HashMap<>(); + consumersMap.put(productId.toString(), List.of(consumerDTO)); + + when(producerService.getProducersByClientId(clientId)).thenReturn(producers); + when(consumerService.getConsumersOfProviders(List.of(productId))).thenReturn(consumersMap); + when(consumerAllowedDataProvidersService.findByDataProviderId(productId)).thenReturn(List.of(productConsumerDTO)); + when(consumerService.findById(consumerId)).thenReturn(Optional.of(consumerDTO)); + + // Act + ProducerConfigDTO result = configurationProvider.getProducerConfigByClientId(clientId, Optional.empty()); + + // Assert + assertNotNull(result); + assertEquals(clientId, result.getClientId()); + assertEquals(1, result.getProducers().size()); + assertEquals(producerId, result.getProducers().get(0).getId()); + assertEquals(1, result.getProducers().get(0).getDataProviders().size()); + + // Verify + verify(producerService).getProducersByClientId(clientId); + verify(consumerService).getConsumersOfProviders(List.of(productId)); + verify(consumerAllowedDataProvidersService).findByDataProviderId(productId); + verify(consumerService).findById(consumerId); + } + + @Test + void getProducerConfigByClientId_withValidClientIdAndProducerId_shouldReturnFilteredConfig() { + // Arrange + List allProducers = List.of(producerDTO); + // Add product to producer's dataProviders list + producerDTO.getDataProviders().add(productDTO); + + Map> consumersMap = new HashMap<>(); + consumersMap.put(productId.toString(), List.of(consumerDTO)); + + when(producerService.getProducersByClientId(clientId)).thenReturn(allProducers); + when(consumerService.getConsumersOfProviders(List.of(productId))).thenReturn(consumersMap); + when(consumerAllowedDataProvidersService.findByDataProviderId(productId)).thenReturn(List.of(productConsumerDTO)); + when(consumerService.findById(consumerId)).thenReturn(Optional.of(consumerDTO)); + + // Act + ProducerConfigDTO result = configurationProvider.getProducerConfigByClientId(clientId, Optional.of(producerId)); + + // Assert + assertNotNull(result); + assertEquals(clientId, result.getClientId()); + assertEquals(1, result.getProducers().size()); + assertEquals(producerId, result.getProducers().get(0).getId()); + + // Verify + verify(producerService).getProducersByClientId(clientId); + verify(consumerService).getConsumersOfProviders(List.of(productId)); + verify(consumerAllowedDataProvidersService).findByDataProviderId(productId); + verify(consumerService).findById(consumerId); + } + + @Test + void getProducerConfigByClientId_withNoMatchingProducers_shouldReturnEmptyConfig() { + // Arrange + when(producerService.getProducersByClientId(clientId)).thenReturn(Collections.emptyList()); + when(consumerService.getConsumersOfProviders(Collections.emptyList())).thenReturn(Collections.emptyMap()); + + // Act + ProducerConfigDTO result = configurationProvider.getProducerConfigByClientId(clientId, Optional.empty()); + + // Assert + assertNotNull(result); + assertEquals(clientId, result.getClientId()); + assertTrue(result.getProducers().isEmpty()); + + // Verify + verify(producerService).getProducersByClientId(clientId); + verify(consumerService).getConsumersOfProviders(Collections.emptyList()); + } + + @Test + void getProducerConfigByClientId_withNoMatchingProducerForSpecificId_shouldReturnEmptyConfig() { + // Arrange + ProducerDTO differentProducer = ProducerDTO.builder() + .id(999L) + .idpClientId(clientId) + .active(true) + .build(); + + when(producerService.getProducersByClientId(clientId)).thenReturn(List.of(differentProducer)); + when(consumerService.getConsumersOfProviders(Collections.emptyList())).thenReturn(Collections.emptyMap()); + + // Act + ProducerConfigDTO result = configurationProvider.getProducerConfigByClientId(clientId, Optional.of(producerId)); + + // Assert + assertNotNull(result); + assertEquals(clientId, result.getClientId()); + assertTrue(result.getProducers().isEmpty()); + + // Verify + verify(producerService).getProducersByClientId(clientId); + verify(consumerService).getConsumersOfProviders(Collections.emptyList()); + } + + @Test + void getProducerConfigByClientId_withNoActiveProducers_shouldReturnEmptyConfig() { + // Arrange + ProducerDTO inactiveProducer = ProducerDTO.builder() + .id(producerId) + .idpClientId(clientId) + .active(false) + .build(); + + when(producerService.getProducersByClientId(clientId)).thenReturn(List.of(inactiveProducer)); + when(consumerService.getConsumersOfProviders(Collections.emptyList())).thenReturn(Collections.emptyMap()); + + // Act + ProducerConfigDTO result = configurationProvider.getProducerConfigByClientId(clientId, Optional.empty()); + + // Assert + assertNotNull(result); + assertEquals(clientId, result.getClientId()); + assertTrue(result.getProducers().isEmpty()); + + // Verify + verify(producerService).getProducersByClientId(clientId); + verify(consumerService).getConsumersOfProviders(Collections.emptyList()); + } + + @Test + void getProducerConfigByClientId_withNullConsumers_shouldInitializeConsumersList() { + // Arrange + List producers = List.of(producerDTO); + // Add product to producer's dataProviders list with null consumers + productDTO.setConsumers(null); // Null consumers list + producerDTO.getDataProviders().add(productDTO); + + Map> consumersMap = new HashMap<>(); + consumersMap.put(productId.toString(), List.of(consumerDTO)); + + when(producerService.getProducersByClientId(clientId)).thenReturn(producers); + when(consumerService.getConsumersOfProviders(List.of(productId))).thenReturn(consumersMap); + when(consumerAllowedDataProvidersService.findByDataProviderId(productId)).thenReturn(List.of(productConsumerDTO)); + when(consumerService.findById(consumerId)).thenReturn(Optional.of(consumerDTO)); + + // Act + ProducerConfigDTO result = configurationProvider.getProducerConfigByClientId(clientId, Optional.empty()); + + // Assert + assertNotNull(result); + assertEquals(clientId, result.getClientId()); + assertEquals(1, result.getProducers().size()); + assertNotNull(result.getProducers().get(0).getDataProviders().get(0).getConsumers()); + + // Verify + verify(producerService).getProducersByClientId(clientId); + verify(consumerService).getConsumersOfProviders(List.of(productId)); + verify(consumerAllowedDataProvidersService).findByDataProviderId(productId); + verify(consumerService).findById(consumerId); + } + + @Test + void getProducerConfigByClientId_withExpiredValidity_shouldNotAddConsumer() { + // Arrange + List producers = List.of(producerDTO); + // Add product to producer's dataProviders list + producerDTO.getDataProviders().add(productDTO); + + // Create expired product consumer relationship + ProductConsumerDTO expiredProductConsumer = ProductConsumerDTO.builder() + .consumerId(consumerId) + .productId(productId) + .validity(BigDecimal.valueOf(30)) // 30 days validity + .grantedTs(Timestamp.from(Instant.now().minus(60, ChronoUnit.DAYS))) // 60 days ago + .build(); + + Map> consumersMap = new HashMap<>(); + consumersMap.put(productId.toString(), List.of(consumerDTO)); + + when(producerService.getProducersByClientId(clientId)).thenReturn(producers); + when(consumerService.getConsumersOfProviders(List.of(productId))).thenReturn(consumersMap); + when(consumerAllowedDataProvidersService.findByDataProviderId(productId)).thenReturn(List.of(expiredProductConsumer)); + + // Act + ProducerConfigDTO result = configurationProvider.getProducerConfigByClientId(clientId, Optional.empty()); + + // Assert + assertNotNull(result); + assertEquals(clientId, result.getClientId()); + assertEquals(1, result.getProducers().size()); + assertTrue(result.getProducers().get(0).getDataProviders().get(0).getConsumers().isEmpty()); + + // Verify + verify(producerService).getProducersByClientId(clientId); + verify(consumerService).getConsumersOfProviders(List.of(productId)); + verify(consumerAllowedDataProvidersService).findByDataProviderId(productId); + verify(consumerService, never()).findById(any()); + } + + @Test + void getProducerConfigByClientId_withValidityButNoGrantedTs_shouldNotAddConsumer() { + // Arrange + List producers = List.of(producerDTO); + // Add product to producer's dataProviders list + producerDTO.getDataProviders().add(productDTO); + + // Create product consumer relationship with validity but no grantedTs + ProductConsumerDTO invalidProductConsumer = ProductConsumerDTO.builder() + .consumerId(consumerId) + .productId(productId) + .validity(BigDecimal.valueOf(30)) // 30 days validity + .grantedTs(null) // No granted timestamp + .build(); + + Map> consumersMap = new HashMap<>(); + consumersMap.put(productId.toString(), List.of(consumerDTO)); + + when(producerService.getProducersByClientId(clientId)).thenReturn(producers); + when(consumerService.getConsumersOfProviders(List.of(productId))).thenReturn(consumersMap); + when(consumerAllowedDataProvidersService.findByDataProviderId(productId)).thenReturn(List.of(invalidProductConsumer)); + + // Act + ProducerConfigDTO result = configurationProvider.getProducerConfigByClientId(clientId, Optional.empty()); + + // Assert + assertNotNull(result); + assertEquals(clientId, result.getClientId()); + assertEquals(1, result.getProducers().size()); + assertTrue(result.getProducers().get(0).getDataProviders().get(0).getConsumers().isEmpty()); + + // Verify + verify(producerService).getProducersByClientId(clientId); + verify(consumerService).getConsumersOfProviders(List.of(productId)); + verify(consumerAllowedDataProvidersService).findByDataProviderId(productId); + verify(consumerService, never()).findById(any()); + } + + @Test + void getProducerConfigByClientId_withConsumerNotFound_shouldNotAddConsumer() { + // Arrange + List producers = List.of(producerDTO); + // Add product to producer's dataProviders list + producerDTO.getDataProviders().add(productDTO); + + Map> consumersMap = new HashMap<>(); + consumersMap.put(productId.toString(), List.of(consumerDTO)); + + when(producerService.getProducersByClientId(clientId)).thenReturn(producers); + when(consumerService.getConsumersOfProviders(List.of(productId))).thenReturn(consumersMap); + when(consumerAllowedDataProvidersService.findByDataProviderId(productId)).thenReturn(List.of(productConsumerDTO)); + when(consumerService.findById(consumerId)).thenReturn(Optional.empty()); + + // Act + ProducerConfigDTO result = configurationProvider.getProducerConfigByClientId(clientId, Optional.empty()); + + // Assert + assertNotNull(result); + assertEquals(clientId, result.getClientId()); + assertEquals(1, result.getProducers().size()); + assertTrue(result.getProducers().get(0).getDataProviders().get(0).getConsumers().isEmpty()); + + // Verify + verify(producerService).getProducersByClientId(clientId); + verify(consumerService).getConsumersOfProviders(List.of(productId)); + verify(consumerAllowedDataProvidersService).findByDataProviderId(productId); + verify(consumerService).findById(consumerId); + } + + // Tests for isValidProvider method through public methods + + @Test + void isValidProvider_withValidityNullShouldBeValid() { + // Arrange + List consumers = List.of(consumerDTO); + ProductConsumerDTO validProductConsumer = ProductConsumerDTO.builder() + .consumerId(consumerId) + .productId(productId) + .validity(null) // Null validity means no expiration + .build(); + + List products = List.of(productDTO); + List producers = List.of(producerDTO); + + when(consumerService.findByIdpClientId(clientId)).thenReturn(consumers); + when(consumerAllowedDataProvidersService.findByConsumerId(consumerId)).thenReturn(List.of(validProductConsumer)); + when(dataProviderService.getProductsByIds(List.of(productId))).thenReturn(products); + when(producerService.getProducersByIds(List.of(producerId))).thenReturn(producers); + + // Act + ConsumerConfigDTO result = configurationProvider.getConsumerConfigByClientId(clientId, Optional.empty()); + + // Assert + assertNotNull(result); + assertEquals(1, result.getProducers().size()); + } + + @Test + void isValidProvider_withValidGrantedTsAndValidity_shouldBeValid() { + // Arrange + List consumers = List.of(consumerDTO); + ProductConsumerDTO validProductConsumer = ProductConsumerDTO.builder() + .consumerId(consumerId) + .productId(productId) + .validity(BigDecimal.valueOf(30)) // 30 days validity + .grantedTs(Timestamp.from(Instant.now().minus(15, ChronoUnit.DAYS))) // 15 days ago, still valid + .build(); + + List products = List.of(productDTO); + List producers = List.of(producerDTO); + + when(consumerService.findByIdpClientId(clientId)).thenReturn(consumers); + when(consumerAllowedDataProvidersService.findByConsumerId(consumerId)).thenReturn(List.of(validProductConsumer)); + when(dataProviderService.getProductsByIds(List.of(productId))).thenReturn(products); + when(producerService.getProducersByIds(List.of(producerId))).thenReturn(producers); + + // Act + ConsumerConfigDTO result = configurationProvider.getConsumerConfigByClientId(clientId, Optional.empty()); + + // Assert + assertNotNull(result); + assertEquals(1, result.getProducers().size()); + } + + // Test for isValidGrantedTs method through isValidProvider + + @Test + void isValidGrantedTs_withFutureDate_shouldBeValid() { + // Arrange + List consumers = List.of(consumerDTO); + ProductConsumerDTO validProductConsumer = ProductConsumerDTO.builder() + .consumerId(consumerId) + .productId(productId) + .validity(BigDecimal.valueOf(30)) // 30 days validity + .grantedTs(Timestamp.from(Instant.now().plus(1, ChronoUnit.DAYS))) // Future date + .build(); + + List products = List.of(productDTO); + List producers = List.of(producerDTO); + + when(consumerService.findByIdpClientId(clientId)).thenReturn(consumers); + when(consumerAllowedDataProvidersService.findByConsumerId(consumerId)).thenReturn(List.of(validProductConsumer)); + when(dataProviderService.getProductsByIds(List.of(productId))).thenReturn(products); + when(producerService.getProducersByIds(List.of(producerId))).thenReturn(producers); + + // Act + ConsumerConfigDTO result = configurationProvider.getConsumerConfigByClientId(clientId, Optional.empty()); + + // Assert + assertNotNull(result); + assertEquals(1, result.getProducers().size()); + } +} \ No newline at end of file From 0525c83083df89bedbdac7a6769c8f567c468ac6 Mon Sep 17 00:00:00 2001 From: Sumeet Raheja Date: Tue, 5 Aug 2025 11:29:09 +0100 Subject: [PATCH 2/3] Added more documentation in README and made mvnw workable --- .mvn/wrapper/MavenWrapperDownloader.java | 117 ++++++ .mvn/wrapper/maven-wrapper.properties | 2 + README.md | 94 ++++- mvnw | 451 +++++++++++++---------- mvnw.cmd | 281 +++++++------- 5 files changed, 605 insertions(+), 340 deletions(-) create mode 100644 .mvn/wrapper/MavenWrapperDownloader.java create mode 100644 .mvn/wrapper/maven-wrapper.properties diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..b901097 --- /dev/null +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..642d572 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/README.md b/README.md index 34fed40..5d6a637 100644 --- a/README.md +++ b/README.md @@ -120,61 +120,108 @@ For development purposes, follow these steps to generate certificates for mTLS. 1. **Generate a Root CA certificate**: ```bash - openssl req -x509 -sha256 -days 3650 -newkey rsa:4096 -keyout rootCA.key -out rootCA.crt + export CA_ROOT_PASSWORD=changeit + openssl req -x509 -sha256 -days 3650 -newkey rsa:4096 \ + -keyout rootCA.key -out rootCA.crt \ + -subj "/C=GB/ST=England/L=London/O=Informed/OU=IT Department/CN=informed.com/emailAddress=admin@informed.com" \ + -passout env:ROOT_PASSWORD ``` + | File | Description | + |---------------|----------------------------------------------------------------------| + | `rootCA.key` | Encrypted private key (RSA 4096-bit). Protected by `$ROOT_PASSWORD`. | + | `rootCA.crt` | Self-signed X.509 certificate (valid for 10 years). | This creates a Root Certificate Authority (CA) that will be used to sign other certificates. The certificate is valid for 10 years (3650 days). + 2. **Generate a host certificate**: ```bash - openssl req -new -newkey rsa:4096 -keyout localhost.key -out localhost.csr -nodes + openssl req -new -newkey rsa:4096 -nodes \ + -keyout localhost.key -out localhost.csr \ + -subj "/C=GB/ST=England/L=London/O=Informed/OU=IT Department/CN=localhost/emailAddress=admin@informed.com" ``` - This creates a private key and certificate signing request (CSR) for the host. + This creates a private key and certificate signing request (CSR) for the host. The private key will **not** be password protected (`-nodes`). + + | File Name | Description | + |------------------|-----------------------------------------------------------------------------| + | `localhost.key` | **Private key** (RSA 4096-bit), unencrypted due to the `-nodes` option. | + | `localhost.csr` | **Certificate Signing Request** — includes public key and subject details. | 3. **Sign the host certificate with the Root CA**: ```bash - openssl x509 -req -CA rootCA.crt -CAkey rootCA.key -in localhost.csr -out localhost.crt -days 365 -CAcreateserial -extfile localhost.ext - ``` - This signs the host CSR with the Root CA, creating a certificate valid for 365 days. - - The content of the `localhost.ext` file should be: - ``` + ( cat < localhost.ext authorityKeyIdentifier=keyid,issuer basicConstraints=CA:FALSE subjectAltName = @alt_names + [alt_names] DNS.1 = localhost DNS.2 = keycloak + EOF + ) && openssl x509 -req \ + -in localhost.csr -CA rootCA.crt -CAkey rootCA.key \ + -CAcreateserial -out localhost.crt -days 365 \ + -extfile localhost.ext && rm localhost.ext ``` - This configuration specifies that the certificate is valid for both `localhost` and `keycloak` hostnames. + When running the command that creates `localhost.ext` temporarily and signs the CSR, the following files are created: + + | File Name | Description | + |-----------------|----------------------------------------------------------------------------------------------------------------------------------------------| + | `localhost.ext` | Temporary extension file specifying certificate extensions and Subject Alternative Names (SANs). Created at start and deleted after signing. | + | `localhost.crt` | The signed certificate generated from `localhost.csr` using the Root CA, valid for 365 days. This certificate is valid for the hostnames `localhost` and `keycloak`. | | + + 4. **Create a PKCS12 keystore for the server**: ```bash - openssl pkcs12 -export -out localhost.p12 -name "localhost" -inkey localhost.key -in localhost.crt + export P12_PASSWORD="changeit" + openssl pkcs12 -export -out localhost.p12 -name "localhost" -inkey localhost.key -in localhost.crt -passout env:P12_PASSWORD ``` - This bundles the host certificate and private key into a PKCS12 format. + When running following files are created + + | File Name | Description | + |---------------|--------------------------------------------------------------------| + | `localhost.p12` | Password-protected PKCS#12 archive containing both the private key (`localhost.key`) and the signed certificate (`localhost.crt`). Used for importing into browsers, servers, or other systems that require combined key and certificate. | + 5. **Create a PEM file for Linux keystore**: ```bash - openssl pkcs12 -in localhost.p12 -clcerts -nokeys -out localhost.pem + openssl pkcs12 -in localhost.p12 -clcerts -nokeys -out localhost.pem ``` - This extracts the certificate (without the private key) in PEM format. + When running following files are created + + | File Name | Description | + |----------------|----------------------------------------------------------------------| + | `localhost.pem` | PEM-format file containing only the client certificate extracted from the PKCS#12 archive (`localhost.p12`). This file does **not** include the private key. It is commonly used for systems that require the certificate in PEM format without the key. | 6. **Add the Root CA to the Trust Store**: ```bash keytool -importcert -file rootCA.crt -alias clientca -keystore localhost.p12 -storetype PKCS12 -storepass changeit ``` - This adds the Root CA to the trust store so that clients signed by this CA will be trusted. + Adds the Root CA certificate as a trusted certificate entry in the PKCS#12 keystore `localhost.p12`. +This can be useful for trusting the CA in applications that read this keystore. 7. **Generate a client certificate**: ```bash - openssl req -new -newkey rsa:4096 -nodes -keyout client.key -out client.csr + openssl req -new -newkey rsa:4096 -nodes -keyout client.key -out client.csr \ + -subj "/C=GB/ST=England/L=London/O=Informed/OU=IT Department/CN=client.informed.com/emailAddress=admin@informed.com" + ``` This creates a private key and CSR for the client. + | File Name | Description | + |-------------|----------------------------------------------------------| + | `client.key` | Private RSA key (4096-bit) generated for the client. | + | `client.csr` | Certificate Signing Request containing client details, used to request a signed certificate from a CA. | + 8. **Sign the client certificate with the Root CA**: ```bash openssl x509 -req -CA rootCA.crt -CAkey rootCA.key -in client.csr -out client.crt -days 365 -CAcreateserial + ``` + | File Name | Description | + |-----------------|-------------------------------------------------------------------------------------------------| + | `client.crt` | Signed client certificate, valid for 365 days, issued by the Root CA based on the CSR (`client.csr`). | + This signs the client CSR with the Root CA, creating a certificate valid for 365 days. 9. **Create a PKCS12 keystore for the client**: @@ -183,12 +230,22 @@ For development purposes, follow these steps to generate certificates for mTLS. ``` This bundles the client certificate and private key into a PKCS12 format for use in browsers or client applications. + | File Name | Description | + |--------------|--------------------------------------------------------------------------------------------------| + | `client.p12` | A PKCS#12 archive containing the client's private key (`client.key`) and X.509 certificate (`client.crt`). This file is password-protected and is commonly used for client authentication in browsers, APIs, and applications. | + + 10. **Create a Java keystore using keytool**: ```bash keytool -importkeystore -destkeystore keystore.jks -srckeystore localhost.p12 -srcstoretype PKCS12 -alias "localhost" ``` This converts the PKCS12 keystore to a Java KeyStore (JKS) format used by Java applications. + | File Name | Description | + |------------------|-----------------------------------------------------------------------------| + | `keystore.jks` | A Java KeyStore (JKS) containing the key and certificate originally in `localhost.p12`. This is used in Java-based applications such as Tomcat, Spring Boot, or Keycloak for SSL/TLS. | + + 11. **Create a Java truststore using keytool**: ```bash keytool -import -trustcacerts -noprompt -alias ca -ext san=dns:localhost,ip:127.0.0.1 -file rootCA.crt -keystore truststore.jks @@ -201,6 +258,11 @@ For development purposes, follow these steps to generate certificates for mTLS. ``` This ensures the Root CA is properly imported into the Java truststore. + | File Name | Description | + |------------------|-------------------------------------------------------------------------------------------------| + | `truststore.jks` | Java TrustStore containing the imported Root CA certificate (`rootCA.crt`) under the alias `ca`. Trusted by Java applications for verifying certificates signed by this CA. | + + 13. **Test mTLS connectivity**: ```bash curl --location 'https://localhost:8443/realms/management-node/protocol/openid-connect/token' \ diff --git a/mvnw b/mvnw index 19529dd..41c0f0c 100755 --- a/mvnw +++ b/mvnw @@ -19,241 +19,292 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.2 +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- -# JAVA_HOME - location of a JDK home dir, required when download maven via java source -# MVNW_REPOURL - repo url base for downloading maven distribution -# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- -set -euf -[ "${MVNW_VERBOSE-}" != debug ] || set -x +if [ -z "$MAVEN_SKIP_RC" ] ; then -# OS specific support. -native_path() { printf %s\\n "$1"; } -case "$(uname)" in -CYGWIN* | MINGW*) - [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" - native_path() { cygpath --path --windows "$1"; } - ;; -esac + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi -# set JAVACMD and JAVACCMD -set_java_home() { - # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched - if [ -n "${JAVA_HOME-}" ]; then - if [ -x "$JAVA_HOME/jre/sh/java" ]; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - JAVACCMD="$JAVA_HOME/jre/sh/javac" - else - JAVACMD="$JAVA_HOME/bin/java" - JAVACCMD="$JAVA_HOME/bin/javac" + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi - if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then - echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 - echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 - return 1 +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" fi fi - else - JAVACMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v java - )" || : - JAVACCMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v javac - )" || : - - if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then - echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 - return 1 - fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` fi -} +fi -# hash string like Java String::hashCode -hash_string() { - str="${1:-}" h=0 - while [ -n "$str" ]; do - char="${str%"${str#?}"}" - h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) - str="${str#?}" - done - printf %x\\n $h -} +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" -verbose() { :; } -[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done -die() { - printf %s\\n "$1" >&2 - exit 1 -} + saveddir=`pwd` -trim() { - # MWRAPPER-139: - # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. - # Needed for removing poorly interpreted newline sequences when running in more - # exotic environments such as mingw bash on Windows. - printf "%s" "${1}" | tr -d '[:space:]' -} + M2_HOME=`dirname "$PRG"`/.. -# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties -while IFS="=" read -r key value; do - case "${key-}" in - distributionUrl) distributionUrl=$(trim "${value-}") ;; - distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; - esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" - -case "${distributionUrl##*/}" in -maven-mvnd-*bin.*) - MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ - case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in - *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; - :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; - :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; - :Linux*x86_64*) distributionPlatform=linux-amd64 ;; - *) - echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 - distributionPlatform=linux-amd64 - ;; - esac - distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" - ;; -maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; -esac + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" -distributionUrlName="${distributionUrl##*/}" -distributionUrlNameMain="${distributionUrlName%.*}" -distributionUrlNameMain="${distributionUrlNameMain%-bin}" -MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" -MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" - -exec_maven() { - unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : - exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" -} + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi -if [ -d "$MAVEN_HOME" ]; then - verbose "found existing MAVEN_HOME at $MAVEN_HOME" - exec_maven "$@" +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi -case "${distributionUrl-}" in -*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; -*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; -esac +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi -# prepare tmp dir -if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then - clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } - trap clean HUP INT TERM EXIT -else - die "cannot create temp dir" +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi fi -mkdir -p -- "${MAVEN_HOME%/*}" +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi -# Download and Install Apache Maven -verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -verbose "Downloading from: $distributionUrl" -verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi -# select .zip or .tar.gz -if ! command -v unzip >/dev/null; then - distributionUrl="${distributionUrl%.zip}.tar.gz" - distributionUrlName="${distributionUrl##*/}" +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." fi -# verbose opt -__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' -[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher -# normalize http auth -case "${MVNW_PASSWORD:+has-password}" in -'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; -has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; -esac +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { -if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then - verbose "Found wget ... using wget" - wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" -elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then - verbose "Found curl ... using curl" - curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" -elif set_java_home; then - verbose "Falling back to use Java to download" - javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" - targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" - cat >"$javaSource" <<-END - public class Downloader extends java.net.Authenticator - { - protected java.net.PasswordAuthentication getPasswordAuthentication() - { - return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); - } - public static void main( String[] args ) throws Exception - { - setDefault( new Downloader() ); - java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); - } - } - END - # For Cygwin/MinGW, switch paths to Windows format before running javac and java - verbose " - Compiling Downloader.java ..." - "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" - verbose " - Running Downloader.java ..." - "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" -fi + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi -# If specified, validate the SHA-256 sum of the Maven distribution zip file -if [ -n "${distributionSha256Sum-}" ]; then - distributionSha256Result=false - if [ "$MVN_CMD" = mvnd.sh ]; then - echo "Checksum validation is not supported for maven-mvnd." >&2 - echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then - distributionSha256Result=true + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break fi - elif command -v shasum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then - distributionSha256Result=true + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` fi - else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 - echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - fi - if [ $distributionSha256Result = false ]; then - echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 - echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 - exit 1 + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; fi -# unzip and move -if command -v unzip >/dev/null; then - unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi else - tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR fi -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -clean || : -exec_maven "$@" +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 249bdf3..8611571 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,4 +1,3 @@ -<# : batch portion @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @@ -19,131 +18,165 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM MVNW_REPOURL - repo url base for downloading maven distribution -@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( - IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) ) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% -@SET __MVNW_PSMODULEP_SAVE= -@SET __MVNW_ARG0_NAME__= -@SET MVNW_USERNAME= -@SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) -@echo Cannot start maven from wrapper >&2 && exit /b 1 -@GOTO :EOF -: end batch / begin powershell #> - -$ErrorActionPreference = "Stop" -if ($env:MVNW_VERBOSE -eq "true") { - $VerbosePreference = "Continue" -} - -# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties -$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl -if (!$distributionUrl) { - Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" -} - -switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { - "maven-mvnd-*" { - $USE_MVND = $true - $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" - $MVN_CMD = "mvnd.cmd" - break - } - default { - $USE_MVND = $false - $MVN_CMD = $script -replace '^mvnw','mvn' - break - } -} - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" -} -$distributionUrlName = $distributionUrl -replace '^.*/','' -$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" -if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" -} -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' -$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" - -if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { - Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" - Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" - exit $? -} - -if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { - Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" -} - -# prepare tmp dir -$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile -$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" -$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null -trap { - if ($TMP_DOWNLOAD_DIR.Exists) { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } - } -} - -New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null - -# Download and Install Apache Maven -Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -Write-Verbose "Downloading from: $distributionUrl" -Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -$webclient = New-Object System.Net.WebClient -if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { - $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) -} -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum -if ($distributionSha256Sum) { - if ($USE_MVND) { - Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." - } - Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash - if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { - Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." - } -} - -# unzip and move -Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null -try { - Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null -} catch { - if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { - Write-Error "fail to move MAVEN_HOME" - } -} finally { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } -} - -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% From c7edae8d9f2dc170e480e95b6a1e05f3a60cf457 Mon Sep 17 00:00:00 2001 From: Sumeet Raheja Date: Wed, 6 Aug 2025 09:35:23 +0100 Subject: [PATCH 3/3] Added more documentation in README and made mvnw workable --- .java-version | 1 + docker/.env | 28 ++++++++++++ docker/{keycloak => }/README.md | 15 ++++--- docker/{keycloak => }/docker-compose.yml | 0 docker/keystore.jks | Bin 0 -> 6478 bytes docker/localhost.crt | 35 +++++++++++++++ docker/localhost.key | 52 +++++++++++++++++++++++ docker/localhost.p12 | Bin 0 -> 6225 bytes docker/truststore.jks | Bin 0 -> 1926 bytes src/main/resources/application.yml | 10 ++--- src/main/resources/keystore.jks | Bin 0 -> 6478 bytes src/main/resources/truststore.jks | Bin 0 -> 1926 bytes 12 files changed, 130 insertions(+), 11 deletions(-) create mode 100644 .java-version create mode 100644 docker/.env rename docker/{keycloak => }/README.md (93%) rename docker/{keycloak => }/docker-compose.yml (100%) create mode 100644 docker/keystore.jks create mode 100644 docker/localhost.crt create mode 100644 docker/localhost.key create mode 100644 docker/localhost.p12 create mode 100644 docker/truststore.jks create mode 100644 src/main/resources/keystore.jks create mode 100644 src/main/resources/truststore.jks diff --git a/.java-version b/.java-version new file mode 100644 index 0000000..aabe6ec --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +21 diff --git a/docker/.env b/docker/.env new file mode 100644 index 0000000..6518abf --- /dev/null +++ b/docker/.env @@ -0,0 +1,28 @@ +# Database configuration +POSTGRES_DB=keycloak_db +POSTGRES_USER=keycloak_db_user +POSTGRES_PASSWORD=keycloak_db_user_password + +# Keycloak admin credentials +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=password + +# SSL configuration +SERVER_SSL_KEY_STORE_PASSWORD=changeit +SERVER_SSL_TRUST_STORE_PASSWORD=changeit +KC_HTTPS_KEY_STORE_PASSWORD=changeit +KC_HTTPS_TRUST_STORE_PASSWORD=changeit +KC_SPI_TRUSTSTORE_FILE_PASSWORD=changeit + +# Keycloak configuration +KC_HOSTNAME=localhost +KC_HOSTNAME_PORT=8080 +KC_HOSTNAME_STRICT_BACKCHANNEL=false +KC_HTTP_ENABLED=false +KC_HOSTNAME_STRICT_HTTPS=false +KC_HEALTH_ENABLED=true +KC_DB=postgres +KC_HTTPS_CLIENT_AUTH=required +KC_HTTPS_ENABLED=true +KC_HTTPS_PORT=8443 +KC_LOG_LEVEL=INFO diff --git a/docker/keycloak/README.md b/docker/README.md similarity index 93% rename from docker/keycloak/README.md rename to docker/README.md index c263779..6020461 100644 --- a/docker/keycloak/README.md +++ b/docker/README.md @@ -71,10 +71,10 @@ Import client key and crt in keystore to create the "certificate" to be used in ## To Test MTLS: curl --location 'https://localhost:8443/realms/management-node/protocol/openid-connect/token' \ ---cert client.crt --key client.key \ ---header 'Content-Type: application/x-www-form-urlencoded' \ ---data-urlencode 'client_id=ztf-client' \ ---data-urlencode 'grant_type=client_credentials' + --cert client.crt --key client.key \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'client_id=ztf-client' \ + --data-urlencode 'grant_type=client_credentials' --- @@ -96,6 +96,8 @@ mvn clean package 1. Set up environment variables in a `.env` file in the docker directory: ``` +cd docker +cat < .env # Database configuration POSTGRES_DB=keycloak_db POSTGRES_USER=keycloak_db_user @@ -124,13 +126,14 @@ KC_HTTPS_CLIENT_AUTH=required KC_HTTPS_ENABLED=true KC_HTTPS_PORT=8443 KC_LOG_LEVEL=INFO +EOF ``` 2. Start all services using Docker Compose: ```bash cd docker -docker-compose up -d +docker compose up -d ``` This will start: @@ -144,7 +147,7 @@ This will start: ```bash cd docker -docker-compose down +docker compose down ``` To remove volumes as well: diff --git a/docker/keycloak/docker-compose.yml b/docker/docker-compose.yml similarity index 100% rename from docker/keycloak/docker-compose.yml rename to docker/docker-compose.yml diff --git a/docker/keystore.jks b/docker/keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..93439f2b8ca0e3e94dc383ffd56eec386f60392c GIT binary patch literal 6478 zcma)BRZtuZlVovs3m)8pdvKRvVR3g41lI)?X9*rWxVr@3;O_43?jGd6tGoLDyQjN{ zsh;VcndhDw4)6y>SO6RcnD`bEg*i+<>;VIS1jqps@4Y){^)Ap(U(Y~+7*{xbG7OIs~jfbgnWa7##z%(ggHW+%40yp z*y;FRAh|XLsN>lR_Q^cp?Mq|Bm(^=8AgRKxuK6Ew>JNy5dOIOhw3j3Og8h1gWUZS` zsu~%$XR=z1j3^5q5Hqr&RNVULV^53vyVI*{pcuP)+D_iW4|^nJTAvGue$+eadeD)tewlWN?fwj{!>SB3;p=xCbi!iaPu0*fBo@gZ(?emS)Ch`Hmjzfx5(s zed?II8wfLTkMR@ImQn7LxEdb)N5}T^CcbZ{i^O5e*z-mMtbc)>vfK^mId%(Tm3`Hl z`)%^LDdljE^Xsrr{HH5g3j7Du{5Ki8$aqk^iS)t=!vgzn|8Zrq)&e+m?-^ZrIMz~LoHhQ>o+2mIe|$?G}dG>hiG z#q`T2*LVZ4+-wYElDwYX)e_j0r!$302kNgdR>_1{m?7qT%eRNA>YX>BG?PL7D#^*K zE}1Flx*2%%j-*%W4aR$^`XF}9)H~(7xzsB5hw~VcJtMLE@oYe z;vAh>m3K>O&Z2MH-yN(Ig+;*`YWW1X`AD$u(qeH|4TYRzk-vxUx_ z1czZFxhKO8i}0s}!kz=9b_m;44QrR{5svI2LU4;88VyTC=U`X35)wa9551As$P4g7W-tdWW3w6R-8pzzRspB zNXoap!2mdoCMq5Wk@nD3@>r>!)HQt^ae^}m=?J`4DKRFN-QD8oymnIK^#(5%=F4^OUiVUNu z$8kJ#nJP+0a3PUJLft&I#XrtT2jjy1z-MMf1ih6(;x+rcHdS=w^7WSm1P4lwaUSwE zxaRf;yy2$wZT*wCM*)JPjZ5AF&nvs&7}yb?fe1FFK>EqVRH*?Y7E6 zPipqnm@8ER<)K7yV8^_aZ%bmr2sR4lYH@vtl>f7p*(!w+F=Y+TIotO3UQ*Eg74`Ok zWz?=&0KzMClbvf43XkBiA`y)sPAIMb*XZn1hNUq?P%U{ssgB;oo-o*HXy6meSPZ4- zF{ap8)uTdEV>ENQ%K4xyk^w`E2lX79hwfAH1#`urc$OGZ-Ihi0)@+ws7CL9Qn+>b6 zD-r?WA(c$np?w>xmEn&`#jiu2p79`+23_UyTuVx>)?AoZvln*EzZD?hBYVqM&PNTy zEuV**%Ywe%Rrnxjj=%kHZe}|24INLcYo*$;%if%*@mr4181)E*oD3E}A=X?M2 zFU2FESW=b-tdiUzSf}alr2dH}V8B|;t=NnbPR$eWR)b8QYr9)#tNt_&+jC2dcH8-?Pf_Dk-bBV2wmGS*edD zFn!e`aOSfFNBP|#qG`ib*Z(;z94!r__DGILXoz{co)De*NGkJ&{i8}EZfc|>AgMe) zlAOB%Q;VUeFI?EHP*`YV)bxYq$F0faqFb}= z7j#b*QwNySDw=&d4<8$Y`O}It#s`X2r~^ZvJj9)9+qFP-D?{Nif|W8+9O9DDKq2g; z*Pp`ZsZLV~-G#uvTQncTi=%MuZZY%M5^BdoAG`C-PqGFh?XpZiROao=$}kiR<4M-d z?e~{8H>TkDsyI|`l;<_+RCvpS0l-cmcc4P-sj=(hc%z}quUx;R)6L~*=dqThbW7^@ z0_TS+b#gTkFA8Pm(wl`Y)e!>j>0Nsn&=dD6gUNpO)II%DTH-Z;xAXysk6YYA)@u4# z%-CW*(UwYHM7Fxs3U|27sCbhNSzTcD`8c3kq{Q(26x6N`CFpSt#J3tn}_X1@! z&8Ii(xgFDq`?Bw(hnuSX0b&J>Sm|M3Py)iwT<9Nt>c2I{kYr*5b(ie?akvk;c}BZ46!NhY=UueE;mX`PQd)-_9y1oL?$sdHc$k# z=hX7gEIn+hUl@@g>^AVBPO`gjjZ#>e-bMbZQRs^m1@WH{z<|n-wFix?CW+qgY%K8E zAHG(Ge}cOZ?a%n(p|_qu!QcwIjh(C_NE>2FJb^t!tjzZW?C4;$A(L-E zCT0;Vfziac>1Yi3DEOTC6u2-BFitS$FsA>Y%|DqdjNAWgA>hP^#|7FtT2gWH^YU;D@Nx@s@C$Hp zfH8CbwFe0?2aK8U4;2FofcXc)|E&Q2FSE!H_LIAtT_eEn=U#LNa1!NB$8GfgV-~sc zE+uc`@6TBUo~W!(2?GPcnAmQRZTCmsmWqgCjq-|j+w7+0(siPYZ?s9g(+;Iah;>cP zOEe?W*QNrRju)YzXyUfl6whsWn4^npjA#)xahMaCZH?=`i@YgP_GzcF5p1N;GN@z- zHj)j6kCG(>*~}45D0MdE0*NlRpPS!pVZQj)g9s>-*gd}$4 z$&x)qGmv(lpdlH|h z@JOt;VL|yLLr;yi0J1kCgjNN`**?nyQoL&aH!-|PDaV9ypw+myFjqm)?ebmdetBef6w% zWa?;_Ngjs`n+2#z>u+);+u!)Nw+hpx%3b$qLj6C4z_yE~hx-*8K00KpD7mDPkZhV< zoP8T1rimg7E#>222v2+=9z1fD(Dd)yySgo1oL%L~Dk~BZi}xzJj-^9g(noBd1y+UI zZ-r{OS7sV|tjs^PitWj>S&u5~Huitk6OL;@w(aiHUl;nUEb@jX9dKXHAnP+vx)Pc9 z^0^6T(0iba_~I5TTXdBiwmxF?#aC9%PE(-hfKn15dW!iS4Z%CL&(u8Fj_eP`evr==;+(&sb7v_@c$NrGM}vz=lH}9zp=%$z z2XNK&Q`!6;9^U$7F{Ww4EVh-7%DI4=%V`pG!>v)+YFp?8t*T|_JKz=tQK~yP26OuO z8gZK@mrH`x_1;*PoSVJI3#z|cVF@=jxn-WF%9GmL))d#V?MMwNVIgtH(ZB7S_TC3* zMGo*k1tQRV?^`>sy&N#SQL(hFDeE=WqS;?YDirW+SE z$q!o1U5I}F%D~?tk9x%mTdwzq)646DHs)nkrc~mcIc>1{SzFoUTFYjqw|fT8?i1+{ z<9hY2yc5#R91?iN_ zCzHHoq;RdSOGB?BS-z&PyNc1xH0eWE?oc-}dn|_t((#i?T~*x~rP^N2ZVwN&;^@i{ zyEJ61p+0&_Yopjxc5>Vx|51!=;-kGnZ}B9v!zx=T7Z02N-UCjDNTUj5sjR97Z4L!Q z8a<7LoK>UzqHNAB7oCAm1dg?YGpBjTs8`eoln#xEg?8;!7wX0bEW&B!ffUY}o$y>x zpzRGw6bT6JJAG@;iZY_y$EgH^H~Ee$!Cy>?$1y`nidzp%tj*KP2y`qAwh%CeNLpcO zC5Y00KPkPjVTT(^m&_Fv`=87CW`mTh#u^eM)tJ%0nGcRNwY=g78II>NQc@L$4(82P z8uWPLmwBT#@6<1w#TdMpw7TG0JY=|1*OS**eHOQ48+SF3n;BJA{-&mJ#%?01ilBLv zc=N)JZkQ~Vvxx7ow@qBe{bZql8H7R>XBreQtr;8l=_wu6{J7^zzB7y1= z_0?_ofP^koah;EV#6tN zLQa#0^J{U!|)fBqgx|y zF)m4YL_-)k#>tRcDNKkTIUn+w(g7Z>+|B3VtOg8f-Yh3iIKuC0Io}<3tgEWaKqqlW zajauaFm9QTT=@z?9Z$OfQ<^x@54wUq@92U$%51iQP%TZS!$5^25vLUw)1S$HvNqwH z&c|9eDUgYgfy5XgA_aJVP7nMX;&BUGzQ{TLrgM)4pEUDdreZsYofbSRK6#)T+Dc)mEj8uhh2s@pZ+mm7LXD6tYe>fo| zl2wloK=aWI+v^=W!Nsrog(7VOwp^d%)NbMtg9hzW@6Eb(Zy~S-wr28GG4~%uW7#cCnT3IP z!h!jbX-Hy{l`sGaj;qjP+RNE1H49O|#_h zCk3f5Gsj4aWHp*74U*UpMj>Y{L_SMj<2v*Lw2tLZvSW{{@Irab#FA~^tTEPRb{E3C zvINdO>0PGfj8!g5B0A*CIPQ*h`J7*payV69BN86-@qz+{=NF^s9wof*zUS%c$cUAl zlgp2>e!EZ+4!7EJcX%C_WpIPR1?v|)9T*qJoS5`lXTv`SgduKvsEYKhi#*$%mBH`gFoKemZ_} zhUMF@!G$0w17;*oEFv1~NHDGCAEQHSgAKbf6Ca~j7Pv8C*=Px)mm@hP<7ff#P_A=B zxY#}qGea9Ar?n%QeS;qf=yV@tXMFByDDgwMLu%|`a!=&TUPQa~m`e5CWTi%q&9L^a zVmAn%&~w;GD(ZXrBVU)^LYqEs#%J^%m4mR3Njm$J)GDd~DL{V0d(2l=@FgA7jc zZ4A*S9GJ!7-(L9WlMc-Nt&4ZF}UEx{dx8i)p3^L?Gf0(gX zUQZM-5@(|nJ6YhPJ%$6KF563)DkbjB)`|&h?Zjr|n-S;#Bp_ubuvHT@nK=CG zR9UN?M>QnVGH{~$uayj_NLxLMT{R^WtA@bX`~6Vmce|Rd5A&jXgN7G=gT?5mgppp& zR6YM|5Wea3Thzlzs(y}f-{^61R|a_Wf2EYt zrpMG7>ALXpsrOO+*+Z1N$u(9dPW#$3fK|`tHC~lmsaL8?mET%=45Z{+^du3(tH>V_MH@O9uVzAN zZR(OTm#PY-;{SQ{yP{{vlopIY{qYNz^$(W??j@9u#BY@R^r<9W+q7bKU(OP=uf5s! zwM7o^in!;jO`)9q&Wwf29+85DY0F<1SNM3(ht|(ogUL$698N3D3S4DysHP?{iSUF% z+^!9?1a`UQqDY&}-XxEknSX?2Fh186A|sk)6Z8IJ`Sg{1VK{~#yAsD&wmrfOx!VHT z7c6Vp{RVsJ%-9vgGd&Y`wETK#i&da+1F!UDKUav1#MmTp{enEB{nw~_gsei@;2A;X zyfTM1G3AAs#-KpXvu4Gi?g{LJ4z&9rwQc7*j=9Xe_`J11e&vj|=5H%CK@DQx3|OSr zgkc3{>;*aPNESC`EW6xrDJsXP14B(w+A4G*=c|1{!1YaDs;hfWpf#;SR1l`;@MDxB zEBQ=(r2G7bUU&9A-cma+!l3K2A-?J7Qv%r(Tmd|C>^&lWDt}r+>V(Dk*7l|nnk+0? z*2SJbV|pr*HYzp--;A3vJ^A^kUMa`FB4%u+RT?9v35`nk?#-gDT4dmxNJVBP1RbFe zTkZG{FFo>X!U3YaTrwm3>8upos1P`(?0&EP`jgOIEcydJV~Y5%ze%?ok+OraaRc6I zXqK>b4dFB&*&9jAAY1e7a#w#}WCL!tY9pI(J_|_cw%{!M$CuOY+n(?V1hGF>Um%`% z>ScywS>*R954armyxH!ZrZ(4@W_{ycI_D{0R+zA!$A6ZHE05RLu(Buu+Jhi-KZFKv?4WU0z7%tH+C%Dlam z9{4gxs7``Uw=(W=Pb2cqDA6r*wO>zF{eyF&)x4*clLMbanS+x9<6l1pSO5YHJQb6- z=ApV*C3syXtGpoyWeV`E?=_Z8QIT86hP5P@U=|)fL^EjQ?92Va3NS70mew^0zaH=N If1afO0WQ8UX#fBK literal 0 HcmV?d00001 diff --git a/docker/localhost.crt b/docker/localhost.crt new file mode 100644 index 0000000..e49a832 --- /dev/null +++ b/docker/localhost.crt @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIGJDCCBAygAwIBAgIUW4B6D9AeEJoCGNhDm0mBsPNx7BcwDQYJKoZIhvcNAQEL +BQAwgZUxCzAJBgNVBAYTAkdCMRAwDgYDVQQIDAdFbmdsYW5kMQ8wDQYDVQQHDAZM +b25kb24xETAPBgNVBAoMCEluZm9ybWVkMRYwFAYDVQQLDA1JVCBEZXBhcnRtZW50 +MRUwEwYDVQQDDAxpbmZvcm1lZC5jb20xITAfBgkqhkiG9w0BCQEWEmFkbWluQGlu +Zm9ybWVkLmNvbTAeFw0yNTA4MDQxNTM0NDFaFw0yNjA4MDQxNTM0NDFaMIGSMQsw +CQYDVQQGEwJHQjEQMA4GA1UECAwHRW5nbGFuZDEPMA0GA1UEBwwGTG9uZG9uMREw +DwYDVQQKDAhJbmZvcm1lZDEWMBQGA1UECwwNSVQgRGVwYXJ0bWVudDESMBAGA1UE +AwwJbG9jYWxob3N0MSEwHwYJKoZIhvcNAQkBFhJhZG1pbkBpbmZvcm1lZC5jb20w +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCJWlZHvU+lmrK2YvnN7oaF ++XO1FXpdXVF1EW1vWJP34wvRTG2mrw/2Q7dwsEPvlPdAwt+wSf/f9nvGSyFWUmCj +8E7AThQvObiKkT1/rEaBEExIKkvB1Lmjhe+ul9XaiSAxb5Sx/AnU+9Hh6yThnjfC +Moo9Mr3+fs/AwwEl1r5VDYic5yP4sn0AAfNOrcc3hCtIoFHGpsjVPScitTwiV3eb +ufD2ZyEFke71mSSXIYIZ51PEg7pCgOuiMed1r63Hi2VlwLK1hdAxyrccH5RNJVYO +dKpAHJmJjNc+jfzKiJwfdvvZy9hFCMdjQoogZeqYCWNldanOO9pJnpWEEd0k6bdv +4u19lUxADj3TCYvOTv4Xp04fNfD0YGeYtuiwAmHrrjwljXnHnTKaDfmdIp2sbSqR +rLbQJSP+rL0bay88D2hUzh7mC6uTSCwx1k4kjDtuIkRNNCbmkzJKkl43AotaP5eg +5He7YFi4l8y/T7DwsDmXshpIiiwmiD8+O8sWKjbeMbVlHWwtxWkWIOYMJXiQaHXz +2wNSoOqVbfmvOHPYDcpLBfXjt2K7gg0iEm9WIy7UI93EucrIONU/m+szMM99mtKB +CzvMrjytFFsZcX+YJ3iUB59D/xsyGkf3uyDBVm3/wCVH3xRkT+vY20bgpZy3KtIP +89jODTIJgXmUuQ+jl/DCOwIDAQABo20wazAfBgNVHSMEGDAWgBSIggsktfaYgHig +D6svGnMfFi2+dzAJBgNVHRMEAjAAMB4GA1UdEQQXMBWCCWxvY2FsaG9zdIIIa2V5 +Y2xvYWswHQYDVR0OBBYEFMqlnswOOQVcRPiODwkgdvkHqIXmMA0GCSqGSIb3DQEB +CwUAA4ICAQCYg+K2+5f5A8nsHFmkuJ/xucDrzSSkixKWg3h2rSTICcQEJ+in9eKo +lHP/8uQN1gmPBBxGC1Arnvg0ewqFz7MYnGTUVWL3ETKUHRkNjWXPj08Vf71MZYDi +CW3IfGrrkbu3YpPiKmsy881Gv2a2mUkixyz18LxPjS5MnfcXaGXY5b/bAFVlLZ3Z +dMPvlbHECDadydWFvEBN2U4dcRFyN/5q0826CvOy+9ezBbvhBfUiY7CNx1rtbZ1R +ngTJgC+/iQqy96SUBcLILoIvYDUyULMUb+zIX/6WIpwdYLTm+w1Bg0VHLhg0G4c1 +ZEM5UfkiCQ/jSS4K9Hd3jsTz8m2UJrIGmUYRQMZdW6eiIWWOgtl6PUNCt6g2DJz1 +/iwJtaxATyZTLw49sWbN6mqxB4n+2RRQyu/GnD8fB+U6BbPfy46G1wK/fOQks2jg +/icMXa34uPkCbXU7JpfoKpphgs0amTDXb49TSa6Ir2s59Rc6YDn80LkNNMqIKlFm +9Nw1rH4UvUpMS5qK3BJ5q4AldrfcNtE0cdrm5Xy+Ko3QiGMsfITSXmhQu7tMH9sW +VUCCdlPU63It+L7yXdG53KcPkWFu0wHkVU1DRxrlc8A80qJTJ2N/uzlMdf/PF0Ls +8Byoydd++GGbgXNTdFSVcjPHPbTCK4OEOToUbAUOi0/rGJ7ctdyz2Q== +-----END CERTIFICATE----- diff --git a/docker/localhost.key b/docker/localhost.key new file mode 100644 index 0000000..df590fe --- /dev/null +++ b/docker/localhost.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCJWlZHvU+lmrK2 +YvnN7oaF+XO1FXpdXVF1EW1vWJP34wvRTG2mrw/2Q7dwsEPvlPdAwt+wSf/f9nvG +SyFWUmCj8E7AThQvObiKkT1/rEaBEExIKkvB1Lmjhe+ul9XaiSAxb5Sx/AnU+9Hh +6yThnjfCMoo9Mr3+fs/AwwEl1r5VDYic5yP4sn0AAfNOrcc3hCtIoFHGpsjVPSci +tTwiV3ebufD2ZyEFke71mSSXIYIZ51PEg7pCgOuiMed1r63Hi2VlwLK1hdAxyrcc +H5RNJVYOdKpAHJmJjNc+jfzKiJwfdvvZy9hFCMdjQoogZeqYCWNldanOO9pJnpWE +Ed0k6bdv4u19lUxADj3TCYvOTv4Xp04fNfD0YGeYtuiwAmHrrjwljXnHnTKaDfmd +Ip2sbSqRrLbQJSP+rL0bay88D2hUzh7mC6uTSCwx1k4kjDtuIkRNNCbmkzJKkl43 +AotaP5eg5He7YFi4l8y/T7DwsDmXshpIiiwmiD8+O8sWKjbeMbVlHWwtxWkWIOYM +JXiQaHXz2wNSoOqVbfmvOHPYDcpLBfXjt2K7gg0iEm9WIy7UI93EucrIONU/m+sz +MM99mtKBCzvMrjytFFsZcX+YJ3iUB59D/xsyGkf3uyDBVm3/wCVH3xRkT+vY20bg +pZy3KtIP89jODTIJgXmUuQ+jl/DCOwIDAQABAoICAD5scRQPpNgV06zylKeUBU10 +TrL4/PDbzX6QGhVlw3IAnUlVG+yKWY3JvuAXK/kB0dF7/5+mMgU+h4hact9Bdyxq +tc/ObHv+FrHbSoDw8eYMWy7dEisYm/oGkCTgWtAETD5LgN/XNTy36e5sKQnlF2BK +CevAWPsF9TOdLLNewofzK6hroDySGh9bw5F5hzXi+qz6N9A50sKfFV8/0QGmkPSU +cD/8JNTcOychn7CUCQXuzXJdj9RTtUO0vIYO/sLWxpGVH2NCWpmWkNvWOldJZbAS +M6IJnXoXKWgDreT6a0IdmF6mtzbAw6WIAzCwQQsyERt2e2MAbrfQvBEW8VOqaLRl +kJFlL/J/zx2ECd0nnU7Oti6q9GGEhGpSpUN/BAH9SPfmjMGKy98f5bdRzFs4flzo +waiYNjap2a0yutfPKyaW3eUu7aWFXHOJ+Q5kKk9v2/PpexOunIwbNLrVy7TR3Ozy +OxtrNjuhYIr9pAvVPUxQ8Wwb5kf2QY3GcqretpuwH7TXns2OxmmpTtPmMcnLJt4v +OdcEHyBCm/4iNC1bo9aZ90FmBXccVXPS5xTSsWrxjBwWvojf9Ezt6lrxsFBZyRd+ +0hwHxXKSWWS01cLENNZsfx+/iS3kDXJ5mGYTH3fs6ZCDgV6kzzBoDV2QErDT2eFz ++fJopFbh9tQ7ACpHD25pAoIBAQC5kAgjm3gw4kzsgcrMAxKKHHLNaugLq/tWG6mc +coAFzPZ5Z+wQC1f2P7FAYYffBZRODcldMpfdR1Pb4kXBPVjJGQ7AAkBhxoVN1cE/ +wtHY13Zrc1a43odKnbM7alB4tiNGpVEnFRaBY3e/wv3ugJtAhgmWw96LUHe1gvFG +tpkPU2ZENQTuQZFKufriSyWbHriWzJd6EQ7mqGh0610qqFCq2f67JyEItjB+cJiZ +nS/p71ccYHy4nbr2u2QvcrWKRMiLiAfw6bGWyiZ0OETLPPaoL0Y5xju4jlwQQXEC +hfZki9cQB4DX1WB9SDwoEbLoq+/8OAdnxoaD3+Pnn9M93+IXAoIBAQC9fYtCmJJN +nFF6DNkvo99SbCrUd4kui9HZgvp7s61YsxLbVmsoyWteFfIKuEd6TWiBKf2K50ch +AX+QrOt+KY9NXLfW8ZhAnk6LO5NcEtDIf3LxCZq4Mdu1jbxNXCqAYPjrAzYcIWAH +vy7F2pAIbGIg0sg8IC31PF7IoYC7F5j4uENbGugONITu/XbEsnVOPehvsgGNnAaG +zLOrISNrD1MrQyCQHk1aeGtofzhB+suFyi2Ke9uOZ9TMusXMvfbLYLcZPtX0yq5Y +GeiunSHakIb4tvmHIYkI+ItRzdw9ekDKtd54jU3bCLdorh604AEQpQbCdNOnYbwX +vXGllzO5dqt9AoIBABAmgQ7YUTWv/JH5GfV3rv1mTHekfl3FsjJkZfa/4HSgyjL2 +4Izk79r24C6CTpkSmz3P5V6/MIiJ1LqmjvuqozedfrMnfwMDjEah/BfYlPsShbQE +bd/ZPl04LIlXT3myII/dg5qrMzI/vgRqrD894kVTZTJCCfSGJGUf6HAHJHs4/wWg +t7Uk78hZN4TKlZNSsS4u0GBHa8yXi7/hXFKCI8M7Bfi0ByQC92WJ1z5HeAwcULCf +lTtcPxGKbcXTbgmCwxGh3U8Sh+pf3cjvvM0TmUQ6GRqeLkR533lquLNHhZJquWUj +wgsG/0boKpQyNigaDuEc30gg90QzEFrwhio0ejECggEAHcim4+/RRyujzthIJunO +B0mDLeQgBtXpRRQGRP1H6OI0u48Ixt9p5d7zLWYrrfiOzYaJGJUjo/d1UosrdKgZ +oyJIrxtIQBfh5paVoeo/MNR/f41v6TXE//NoPoyXDZiwZ1gY4DEKxAzz0Bt7eid3 +AQvAd1rjfmFf0I86Ca462BU2WRKQmPL+VgY27dHTCFpIt422GskEaZmxP9u0knZV +Lz6c1qo8pFS7JTCh6hT+emf94n93UhnV49wTjQvqx6rK/MfYn2JgJu7jmNaP5KfE +30D7VZcaz/MpCtGdpXgayQHvFum27A+hrG153Bo9cJ1Pw67TOcLt6plXTJzIf4U8 +jQKCAQAe7xCFIPTLL68VPYZqCv0ga2AcQx9DFo7jJGXkZmXhqhmlXsHI75K8HU8Q +zGtOm8cwiByrCyGG5lVmKWZng/3jdfRcRjxQ8WaG5pl3ICaTSrxzVzAnErq5qu8l +hm42BHPlWVKkiezjj1IHpWetQ7WcVW2W2CwzZvOyL5U74zTmC+xuq7WoGdh43Cpw +def/oQNZ3ka0aGsVML/woY10ffi8mfKQix59cLb2Q2MOotjkVNd/svkH8HxQRSIn +Znl1K1tUDZTJeEzwmABp2RAX6mKu7Thwm24AYLGK+nkZziQ2oSlDJiRFhYppbySR +QgvXZBD7vSN4U2MIHv19G4ow9xPA +-----END PRIVATE KEY----- diff --git a/docker/localhost.p12 b/docker/localhost.p12 new file mode 100644 index 0000000000000000000000000000000000000000..ead8d60b5c50141e9d2c2d64614db07cf8d141c9 GIT binary patch literal 6225 zcmaJ_Ra6@clO+%&5InfMLvXhbY0%;>!Hc_t045+mjBFnmMz;Nz-hhT-v;ChG zHaRd18}gSX_-kMcod4|tV*^n0!$9@WFi;KD@ZSz}T&OCP<3A7#r3Pa8C6~rtCBSu2 zQL#{W)RqOa%p3lHF zOiP&_M}Kc7$^<>ylQ{e1jnRnr{Z6s0rLx)d@#WH$+uzl_d~e8|*SWd{E`2dwnk(MY zs`$ozTFyyR?wz4@shc6SvZ*!jc>P03+^4O}44mxtdbje?8N&OBZ$f85k$%xX&kU#p z9J=}E4h?fRAcGkJ)PV&tjoi!V65XOMEd|1ze51T~N1#l!^tRubH=hb}Z3``!GmA*{ zTCsh_HQ95-b-x&g9yODqjF>wCqu$8EYh9tlbxjJuE`sc~b{>Jdb1A9Uayo*0Yh9wl z>b#FmG=%E;y|bh2!o?Wm$d%oMozG%hl$y*G+5_YOAJeV=EVu^bk6u8kzZ#%+Xqi-8 zFM5AJqM#cO;Zg7=LnvRzy@@;Zs=O5TnEV_ye*Op$N?OqGsmf#?RF)hUrL=8Q9aP0Y zmQxd=5L~^jB=9SR>J$XC;{R@EDu(%+wiF$1@|wSxeS;yVaoXtbttEpl!VqKqpkJvu zJxQm++9%Z>zePh(VWwXWg0(#a|UKIo)mFbtU`Ii24=fk?tbpAQbZ3G<^nLh}pPa>tf>Ak$C>43yN z9VMBge>D;vm47?1wbB04l}Oc+C>aru&e%k*0FJ9<=O12`cL;joIu~oeni>?cMIxkK0$7o?`+OYny% zm_h=_TuPNwz;}uX30Z}~v4VPu*r`mT+E?g_e6{k6yPv1H%H$?cCptq0E42dt zc`ni$D0}Qtk(@H}&OEkq_Ej4~(K1z31GO@DH$UQ!>o_G&H7 zP|#nh5i4KBr9D(bFLGU^z8%Wa;(9cKd;B)9#y!Gy?DhMog&nIfOq{q+L$V<;P1Tk! zv-ONAl$W`JDO72f@v@@S4j5Ry`0ZmC zM!nW9iXj#JjQXu}LZWACaHS?9g^gR5MuNsdSpd4GK)`PJe*XM@_||=C(rxE~aNU^^ z8RfhOi^I^mdi1F!IIVb`K1`%mQ;%$5J?!UtKA>r|Vc?KkkBM>PBYHP&}sZ3*sLnCN71}8Pf?i$E@XDnLgJO7dK6lkxpPp+m4BoO9g zafC)T&D{$MI@A*7wnV?p%2qu*d_pEWr!Gf)ge~$CcxWI?clIto%*@z@ho;*g=d+TB zPx+vt5N93b3PMpXo(*8$QrU;bI-zUBa#_;n+3XK;y!uL5I%Z$Jdn|GAl@xA|G(g3d zZ$WiI?mA2R`V&EP+!iZl?#+51HQmkGcCCUt zvet5}rBqfU*qC6#N<L7AA!hUL6#$pT&axW03ilg-MD@c|6Dhe@(WDtmD za)cSQ9ioa@T1+@7EM$aJB8%DTn1&7zhDsT8TF9?x9#{NV9Pel(26Xhc>A^x$rWpr0 ziFgQP4^47Y)oudC~>d@<$wJsG;;zf7ddWe!Dv%knP7bY~=%? z)74K<*R7$e8Kwvo2ald?LiA({FFoY5((POgCTW63BYO7wgzS|zb?naTh2&@fDQC7B z!QWql&ue6BZ^nzJ>Iijm>WgjOz$O0>brWsoLN!m0S&n(WzaSNoEZ?`(k4)(T_Z)cG zdYm;1UH5u_IX|}J1dMoXSP)CFwO`n1dtS`ze>Dxc)53mULzU^ZTighFP7^(p=7&KU{?%t72tS0D z2*m}(9mNL4>aTzGH}XdDfinH4goK|I2+@P<>PlzDx%yJzipd%+DDlgg{{s!ft^czSikN*lJ z!*^ltN-^lq`kVa6srZg$VGLS{AD(UpPk*)&OlxBQyxx(SjSPuAS2NiR4^Ey#ugTaER7PnL1ZD`B5tmHa)eLpBjJ%MNuW zG2Fqo(=?S*KdN#oQr9-|>G-OX;$$DyrQ%$OnaMJG10K$m_(wcN9%F$1-{I(F$*t`V zL-U@3yAM+8&#}o5Kj0bM4CfKq@@<{p#)esF0Qhf=BwUV(8e@@#fN(IgxN$$%UsMOIahucuJ+qX5}NP zQ5G!yq0%>1qJrTZ(sbutfp0XmEDU4&#IOgp^+fLF5QpOSCIl}V(7P+aUfxq4;^GRz z6X!x#r=F5!vn;{WIf>Zcu0v1)k2(Ix3-+0#HOC?MX+*;!M#V&&BZdVv7AHr|u5-W( zMN~2XZZJ_KK+LgRFYu_k0KE%m*(x29R%j1{95=$cOIv_>m`+b0i6b*HYag6TwsVjD2lT%!o{3i#e z$9~W1KK7VC8;{mfzZ<(eP;Kl-W>KcnOdOl=w*3>f>5PPWi)N4*evQ86#G;k3a^i0r zYvDh;Y^Fc`i>RoSKA%M=UegWz+*HP9xa%lRmE~72@16lU5)7E{6j&92otaUiD&EgE zKi|kkFjKf$pD&uYqsAPU9bfwwbm^U~k_(SjTIy?#uQvx3@B|d3lxz*M?p@)&em>^S zH7z0$lEGKmEgpG6vo`bRvKZ*9scJsWssQ^oOz>&m{XqU2l;LzT+hP&P)~G-pac?wh z%)$LSZEa3HTDBz6N5>tn2-W%eg{`F1{8yZJ!?@Tf@@tt}c0CK+sluXaH5aM0b1rmp zbNd?0ZZhtp?Ai71S6;9TE&z+SlIw@P<@WWMmjS6y+bW*9R4`F|WnbDJU&_X|+6&ay zy%&@kywYeyo7#j=MX21+ASM$Nw+ElIOIHoUjwaJsVWcFH`Vo(b?>TTiv0Q1cUOAYa z6O!`T*E3W7vaHOZdUKCQZYzCN1ev%syo9hvY7!aqblVn@IMqKp&rFO_YJc>73kc)= zW5eJ6{#9$>Dpq_P-&(Nu8go2k0n5R;-v+JSW|DE^t#15&U8H5JgcU=7$ioIR$?p zMO%=8{gbub?|I_?)UUFg(hX8RG*qk(;Y-9XN6t1zV(>jyfmJ;wX}8 zrDyMHn}w-Ob68&!!F1#gd(6%pi%6Epi9!$ZeICYUS<2^wEQXEnSEFEfUtT~3kk zgD+vIb!&lsHS}NwLSe3oiaQCt z_A=krpKMN}`D~KBo=y}!Ue<_b=>OccvYk)bRD)@M!My*VI9F~_Mq9x#v(PDj{+_<+ z!3Vx?M)R<}(G0XMZ3uTuQEl{9Ir>hlt4}5(I@~w&IVolj?V`pC*rG-m)&ytra_0khbMVBlX!#yyhbx9s zjnSW!nZ4IV3|tHl(Olj;5eBoSBZ={a{w^T(bpd!&FH#gf3c3#T*UWpAxyK*T~%S zD%DGe-egvTKlpVWLg>0|(dMqWG^cK=C|YeQ*69gMRqHeU?^`|CR9Yz4M||P7&$u<7 z+}NE*Cjr_bZ%Ilxrf<1F-YuwN{*-2?s{V;;%1Ncuho7G&9ZK`zQX?}CnYqW0@*9$V zxL)#(s?drhm>;}rqQ;CjICfk4YsDG+hr0COfbjDAtp!UL#B=IRs`zEM#2g>hr@Asb zIeIHkrpy}}!3WQj+bB(Dztw|kf^xo1ShQ8nVKr&N3Je3kG|C0fLbZV@``1a%E$j@1 zjf9F)-jH(BW)u4y|Cbu88$5k1!;i|#=yr_VdwZC&ok?QUZl~0a(Mw(;*JooOGFANe zQ(F_MKLfsT`cbCa1i^#3zGy}+JcJ@#Zgp-bzv_RdJ4wA1}jZJQb;xWZlPIGV+o1MK4caGS8 zkhx9_J*dX1vo`nh!bfI5W}~3T)@#P~g-$ml&pe=y5x^O;T)x%BNaMfHkLx}MtD(uf zDx)Wj4hFfiY*^FoH6$_3sTK!$NEZrC=%j%T_vudKf zN8stLJqzCGpL?iw`0?H za-EY?V5a_NNJ!_ugDW%pc*5<}21EznmEMwC!%trDoJpz1;|I%y6|bH+r_quW^0vm# zOf8Kn%^V*ZbEt{_PBccw{G&gFe)X{e^Lg$1I;`U^;?`a?ubqSZlTj*SaA-lcEzdo} zGMs#iOd6>Wtb86vhxg*@8)AqDp?vwnGqJ8@;pEV0C%l)lwZJ(?Pdfdc?ST3`_QR<@ zE^O9{Lh_?Hc}k#)gw43uF`m$xc7uUw33*R*8rr?fjY0JU{lOwNyVI>}qiT2RSi?vE zu3xc))@1eX`)>m*Cpy1ti6%odCJ2XNQ_fd$qb)*1Lq`wlQd(NvvfEOB|zfih=zB9dBqq*)Y#yo8@9Ds zDzlt+$2=lcV8(2;+Bb+v$T|Lf8p=tmRyuN>0ZVfcz-f)!As4TY%*DGHHqyqr)RXG*#gGE0Ru3C2P_5&Duzgg_YDCD0ic2h9Rz|088Cte6)=Ja5e5k=hDe6@ z4FLxRpn?YiFoFjD0s#Opf(G{n2`Yw2hW8Bt2LUi<1_>&LNQU+thDZTr0|Wso1Q53o4P&h&mHbH)F;#itnK6KZ27t)Vi2^kHaMBrk3SaBu2#;?) zLGaKLeq%tZqWb!OBAi1FodJY^W#SnK5sS)7+iVP*IjS9QxZ#+Px24F4?*eyyPi+|d zVkJUze~Dt%b<^LDL5#9rI0H1igA@FVl}qzn`t}4)vM8OkF>paB6#fCmdtptR05@`8 z{*Z6thVwu@!Tg-4nvC0lSf(bzzsxmT|&atrmC={yUR$pefTXAvg*%@=^mMY$LAo_t_5{wD(NTdQ8}!7;p>)7qhps zsxe4h^J6wh@YCabAe(+NYyG*12M_XYX~PC5qyj+mSQ)@=z~bfyQrAkFi$#+Fh43c! z(Sy*X$gFxleo#!{Uae8?D%(q-p)mexyqQugP%!Y%k4)CoMtj7EvznKIAWBv`PRl$a ziKr6w51LAt&)h8kDK3Wht>yUtj1C12=a;n)Lt zk+)l{CB;&z)dQwlipLb^3D^8|vN-^6S1mV`!PH}_C_M3y@ zm&`Ivs?T(}u))>;ow|n1DtlvDj0ts<0RaYNl{xB^r)_>c}^E=Cnn*b~`lA3yDc zlTT;K|Ebep+xL@s{8svq-#^V~>KuS?hnkPw0(iet1T7xt@~5koq;F-&Ort`bS~BXp zaFly~kb#bu8r!Dt0~vea$f#g--P&P~7|LJ3$9P-&bHIqqvk|6&`({hz=U3+$2;Y3m zfPX$2+maB0Ui7LG_;EEYsVq0A7_$}9lEb8M&p}XRG-6BYm#m)&_7?ACttiI z%R4pzF($%&u+23_8Gwzr)-w_SzIAENE%b2YP*obl-CL6TAgWffQ>z^NTfYM)VgHni zamI@rLrK!v#bVZW`z(7$itB4l345}t1O>$jKocq5E5bw$ixOA94c;8e{NL#!U4J#t z^4P~HOaQ#C1-=hTac2Xf!uF+Iq0SDgwuDS{{uEz7!-8lzkOj@?MsEs|* z0~m#Z32DHfAB-H6I<2+TL(rXlebcAUyPD`G4N2$_?R;_Iq@^*8jIGT(E~_5&@Q_7Y z9$-iaI1lEWX4hrV0*T_6Ej`0?5UiYr5-C12is+M2d9=R%D~rJlb|@^snmiyS&7YfY zlQvp2_ZT%mTe(lR<%kE2XH^wS+V^kJr>i+nIb4exM}%bTa)-~qnc=_{9pM)eb}<_9 zC9or=3k209BO6>9jlQjXi-=$SJn-H6gF|d$xYWoB;Z^$>2^_sfshnd1<+LD(Bmv;4pQz3tL7KHFGT*LL1% zm`cz~sj=KAuah`#2fTs%+?E5ekoG*H>@+)5CRz1;n}0A(FflL<1_@w>NC9O71OfpC z00baB8Hm!DuY0kS4x(f8S*~~uAIi@E$kkJ#b;K)xS7J5<6bYEpaxfae(nmaK{0t=R M2=(CX6aoS#5cboEng9R* literal 0 HcmV?d00001 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7ad2d89..02be00e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -22,7 +22,7 @@ spring: datasource: url: jdbc:postgresql://localhost:5433/postgres username: keycloak_db_user - password: + password: keycloak_db_user_password jpa: properties: hibernate: @@ -35,11 +35,11 @@ server: port: 8090 ssl: key-alias: localhost - key-store: keystore.jks + key-store: classpath:keystore.jks key-store-type: JKS - key-store-password: - trust-store: truststore.jks - trust-store-password: + key-store-password: changeit + trust-store: classpath:truststore.jks + trust-store-password: changeit trust-store-type: JKS # Actuator Configuration management: diff --git a/src/main/resources/keystore.jks b/src/main/resources/keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..93439f2b8ca0e3e94dc383ffd56eec386f60392c GIT binary patch literal 6478 zcma)BRZtuZlVovs3m)8pdvKRvVR3g41lI)?X9*rWxVr@3;O_43?jGd6tGoLDyQjN{ zsh;VcndhDw4)6y>SO6RcnD`bEg*i+<>;VIS1jqps@4Y){^)Ap(U(Y~+7*{xbG7OIs~jfbgnWa7##z%(ggHW+%40yp z*y;FRAh|XLsN>lR_Q^cp?Mq|Bm(^=8AgRKxuK6Ew>JNy5dOIOhw3j3Og8h1gWUZS` zsu~%$XR=z1j3^5q5Hqr&RNVULV^53vyVI*{pcuP)+D_iW4|^nJTAvGue$+eadeD)tewlWN?fwj{!>SB3;p=xCbi!iaPu0*fBo@gZ(?emS)Ch`Hmjzfx5(s zed?II8wfLTkMR@ImQn7LxEdb)N5}T^CcbZ{i^O5e*z-mMtbc)>vfK^mId%(Tm3`Hl z`)%^LDdljE^Xsrr{HH5g3j7Du{5Ki8$aqk^iS)t=!vgzn|8Zrq)&e+m?-^ZrIMz~LoHhQ>o+2mIe|$?G}dG>hiG z#q`T2*LVZ4+-wYElDwYX)e_j0r!$302kNgdR>_1{m?7qT%eRNA>YX>BG?PL7D#^*K zE}1Flx*2%%j-*%W4aR$^`XF}9)H~(7xzsB5hw~VcJtMLE@oYe z;vAh>m3K>O&Z2MH-yN(Ig+;*`YWW1X`AD$u(qeH|4TYRzk-vxUx_ z1czZFxhKO8i}0s}!kz=9b_m;44QrR{5svI2LU4;88VyTC=U`X35)wa9551As$P4g7W-tdWW3w6R-8pzzRspB zNXoap!2mdoCMq5Wk@nD3@>r>!)HQt^ae^}m=?J`4DKRFN-QD8oymnIK^#(5%=F4^OUiVUNu z$8kJ#nJP+0a3PUJLft&I#XrtT2jjy1z-MMf1ih6(;x+rcHdS=w^7WSm1P4lwaUSwE zxaRf;yy2$wZT*wCM*)JPjZ5AF&nvs&7}yb?fe1FFK>EqVRH*?Y7E6 zPipqnm@8ER<)K7yV8^_aZ%bmr2sR4lYH@vtl>f7p*(!w+F=Y+TIotO3UQ*Eg74`Ok zWz?=&0KzMClbvf43XkBiA`y)sPAIMb*XZn1hNUq?P%U{ssgB;oo-o*HXy6meSPZ4- zF{ap8)uTdEV>ENQ%K4xyk^w`E2lX79hwfAH1#`urc$OGZ-Ihi0)@+ws7CL9Qn+>b6 zD-r?WA(c$np?w>xmEn&`#jiu2p79`+23_UyTuVx>)?AoZvln*EzZD?hBYVqM&PNTy zEuV**%Ywe%Rrnxjj=%kHZe}|24INLcYo*$;%if%*@mr4181)E*oD3E}A=X?M2 zFU2FESW=b-tdiUzSf}alr2dH}V8B|;t=NnbPR$eWR)b8QYr9)#tNt_&+jC2dcH8-?Pf_Dk-bBV2wmGS*edD zFn!e`aOSfFNBP|#qG`ib*Z(;z94!r__DGILXoz{co)De*NGkJ&{i8}EZfc|>AgMe) zlAOB%Q;VUeFI?EHP*`YV)bxYq$F0faqFb}= z7j#b*QwNySDw=&d4<8$Y`O}It#s`X2r~^ZvJj9)9+qFP-D?{Nif|W8+9O9DDKq2g; z*Pp`ZsZLV~-G#uvTQncTi=%MuZZY%M5^BdoAG`C-PqGFh?XpZiROao=$}kiR<4M-d z?e~{8H>TkDsyI|`l;<_+RCvpS0l-cmcc4P-sj=(hc%z}quUx;R)6L~*=dqThbW7^@ z0_TS+b#gTkFA8Pm(wl`Y)e!>j>0Nsn&=dD6gUNpO)II%DTH-Z;xAXysk6YYA)@u4# z%-CW*(UwYHM7Fxs3U|27sCbhNSzTcD`8c3kq{Q(26x6N`CFpSt#J3tn}_X1@! z&8Ii(xgFDq`?Bw(hnuSX0b&J>Sm|M3Py)iwT<9Nt>c2I{kYr*5b(ie?akvk;c}BZ46!NhY=UueE;mX`PQd)-_9y1oL?$sdHc$k# z=hX7gEIn+hUl@@g>^AVBPO`gjjZ#>e-bMbZQRs^m1@WH{z<|n-wFix?CW+qgY%K8E zAHG(Ge}cOZ?a%n(p|_qu!QcwIjh(C_NE>2FJb^t!tjzZW?C4;$A(L-E zCT0;Vfziac>1Yi3DEOTC6u2-BFitS$FsA>Y%|DqdjNAWgA>hP^#|7FtT2gWH^YU;D@Nx@s@C$Hp zfH8CbwFe0?2aK8U4;2FofcXc)|E&Q2FSE!H_LIAtT_eEn=U#LNa1!NB$8GfgV-~sc zE+uc`@6TBUo~W!(2?GPcnAmQRZTCmsmWqgCjq-|j+w7+0(siPYZ?s9g(+;Iah;>cP zOEe?W*QNrRju)YzXyUfl6whsWn4^npjA#)xahMaCZH?=`i@YgP_GzcF5p1N;GN@z- zHj)j6kCG(>*~}45D0MdE0*NlRpPS!pVZQj)g9s>-*gd}$4 z$&x)qGmv(lpdlH|h z@JOt;VL|yLLr;yi0J1kCgjNN`**?nyQoL&aH!-|PDaV9ypw+myFjqm)?ebmdetBef6w% zWa?;_Ngjs`n+2#z>u+);+u!)Nw+hpx%3b$qLj6C4z_yE~hx-*8K00KpD7mDPkZhV< zoP8T1rimg7E#>222v2+=9z1fD(Dd)yySgo1oL%L~Dk~BZi}xzJj-^9g(noBd1y+UI zZ-r{OS7sV|tjs^PitWj>S&u5~Huitk6OL;@w(aiHUl;nUEb@jX9dKXHAnP+vx)Pc9 z^0^6T(0iba_~I5TTXdBiwmxF?#aC9%PE(-hfKn15dW!iS4Z%CL&(u8Fj_eP`evr==;+(&sb7v_@c$NrGM}vz=lH}9zp=%$z z2XNK&Q`!6;9^U$7F{Ww4EVh-7%DI4=%V`pG!>v)+YFp?8t*T|_JKz=tQK~yP26OuO z8gZK@mrH`x_1;*PoSVJI3#z|cVF@=jxn-WF%9GmL))d#V?MMwNVIgtH(ZB7S_TC3* zMGo*k1tQRV?^`>sy&N#SQL(hFDeE=WqS;?YDirW+SE z$q!o1U5I}F%D~?tk9x%mTdwzq)646DHs)nkrc~mcIc>1{SzFoUTFYjqw|fT8?i1+{ z<9hY2yc5#R91?iN_ zCzHHoq;RdSOGB?BS-z&PyNc1xH0eWE?oc-}dn|_t((#i?T~*x~rP^N2ZVwN&;^@i{ zyEJ61p+0&_Yopjxc5>Vx|51!=;-kGnZ}B9v!zx=T7Z02N-UCjDNTUj5sjR97Z4L!Q z8a<7LoK>UzqHNAB7oCAm1dg?YGpBjTs8`eoln#xEg?8;!7wX0bEW&B!ffUY}o$y>x zpzRGw6bT6JJAG@;iZY_y$EgH^H~Ee$!Cy>?$1y`nidzp%tj*KP2y`qAwh%CeNLpcO zC5Y00KPkPjVTT(^m&_Fv`=87CW`mTh#u^eM)tJ%0nGcRNwY=g78II>NQc@L$4(82P z8uWPLmwBT#@6<1w#TdMpw7TG0JY=|1*OS**eHOQ48+SF3n;BJA{-&mJ#%?01ilBLv zc=N)JZkQ~Vvxx7ow@qBe{bZql8H7R>XBreQtr;8l=_wu6{J7^zzB7y1= z_0?_ofP^koah;EV#6tN zLQa#0^J{U!|)fBqgx|y zF)m4YL_-)k#>tRcDNKkTIUn+w(g7Z>+|B3VtOg8f-Yh3iIKuC0Io}<3tgEWaKqqlW zajauaFm9QTT=@z?9Z$OfQ<^x@54wUq@92U$%51iQP%TZS!$5^25vLUw)1S$HvNqwH z&c|9eDUgYgfy5XgA_aJVP7nMX;&BUGzQ{TLrgM)4pEUDdreZsYofbSRK6#)T+Dc)mEj8uhh2s@pZ+mm7LXD6tYe>fo| zl2wloK=aWI+v^=W!Nsrog(7VOwp^d%)NbMtg9hzW@6Eb(Zy~S-wr28GG4~%uW7#cCnT3IP z!h!jbX-Hy{l`sGaj;qjP+RNE1H49O|#_h zCk3f5Gsj4aWHp*74U*UpMj>Y{L_SMj<2v*Lw2tLZvSW{{@Irab#FA~^tTEPRb{E3C zvINdO>0PGfj8!g5B0A*CIPQ*h`J7*payV69BN86-@qz+{=NF^s9wof*zUS%c$cUAl zlgp2>e!EZ+4!7EJcX%C_WpIPR1?v|)9T*qJoS5`lXTv`SgduKvsEYKhi#*$%mBH`gFoKemZ_} zhUMF@!G$0w17;*oEFv1~NHDGCAEQHSgAKbf6Ca~j7Pv8C*=Px)mm@hP<7ff#P_A=B zxY#}qGea9Ar?n%QeS;qf=yV@tXMFByDDgwMLu%|`a!=&TUPQa~m`e5CWTi%q&9L^a zVmAn%&~w;GD(ZXrBVU)^LYqEs#%J^%m4mR3Njm$J)GDd~DL{V0d(2l=@FgA7jc zZ4A*S9GJ!7-(L9WlMc-Nt&4ZF}UEx{dx8i)p3^L?Gf0(gX zUQZM-5@(|nJ6YhPJ%$6KF563)DkbjB)`|&h?Zjr|n-S;#Bp_ubuvHT@nK=CG zR9UN?M>QnVGH{~$uayj_NLxLMT{R^WtA@bX`~6Vmce|Rd5A&jXgN7G=gT?5mgppp& zR6YM|5Wea3Thzlzs(y}f-{^61R|a_Wf2EYt zrpMG7>ALXpsrOO+*+Z1N$u(9dPW#$3fK|`tHC~lmsaL8?mET%=45Z{+^du3(tH>V_MH@O9uVzAN zZR(OTm#PY-;{SQ{yP{{vlopIY{qYNz^$(W??j@9u#BY@R^r<9W+q7bKU(OP=uf5s! zwM7o^in!;jO`)9q&Wwf29+85DY0F<1SNM3(ht|(ogUL$698N3D3S4DysHP?{iSUF% z+^!9?1a`UQqDY&}-XxEknSX?2Fh186A|sk)6Z8IJ`Sg{1VK{~#yAsD&wmrfOx!VHT z7c6Vp{RVsJ%-9vgGd&Y`wETK#i&da+1F!UDKUav1#MmTp{enEB{nw~_gsei@;2A;X zyfTM1G3AAs#-KpXvu4Gi?g{LJ4z&9rwQc7*j=9Xe_`J11e&vj|=5H%CK@DQx3|OSr zgkc3{>;*aPNESC`EW6xrDJsXP14B(w+A4G*=c|1{!1YaDs;hfWpf#;SR1l`;@MDxB zEBQ=(r2G7bUU&9A-cma+!l3K2A-?J7Qv%r(Tmd|C>^&lWDt}r+>V(Dk*7l|nnk+0? z*2SJbV|pr*HYzp--;A3vJ^A^kUMa`FB4%u+RT?9v35`nk?#-gDT4dmxNJVBP1RbFe zTkZG{FFo>X!U3YaTrwm3>8upos1P`(?0&EP`jgOIEcydJV~Y5%ze%?ok+OraaRc6I zXqK>b4dFB&*&9jAAY1e7a#w#}WCL!tY9pI(J_|_cw%{!M$CuOY+n(?V1hGF>Um%`% z>ScywS>*R954armyxH!ZrZ(4@W_{ycI_D{0R+zA!$A6ZHE05RLu(Buu+Jhi-KZFKv?4WU0z7%tH+C%Dlam z9{4gxs7``Uw=(W=Pb2cqDA6r*wO>zF{eyF&)x4*clLMbanS+x9<6l1pSO5YHJQb6- z=ApV*C3syXtGpoyWeV`E?=_Z8QIT86hP5P@U=|)fL^EjQ?92Va3NS70mew^0zaH=N If1afO0WQ8UX#fBK literal 0 HcmV?d00001 diff --git a/src/main/resources/truststore.jks b/src/main/resources/truststore.jks new file mode 100644 index 0000000000000000000000000000000000000000..ebbbd6a82d7555f32947905f256911b64e32b2a4 GIT binary patch literal 1926 zcmV;12YL7~f(L>E0Ru3C2P_5&Duzgg_YDCD0ic2h9Rz|088Cte6)=Ja5e5k=hDe6@ z4FLxRpn?YiFoFjD0s#Opf(G{n2`Yw2hW8Bt2LUi<1_>&LNQU+thDZTr0|Wso1Q53o4P&h&mHbH)F;#itnK6KZ27t)Vi2^kHaMBrk3SaBu2#;?) zLGaKLeq%tZqWb!OBAi1FodJY^W#SnK5sS)7+iVP*IjS9QxZ#+Px24F4?*eyyPi+|d zVkJUze~Dt%b<^LDL5#9rI0H1igA@FVl}qzn`t}4)vM8OkF>paB6#fCmdtptR05@`8 z{*Z6thVwu@!Tg-4nvC0lSf(bzzsxmT|&atrmC={yUR$pefTXAvg*%@=^mMY$LAo_t_5{wD(NTdQ8}!7;p>)7qhps zsxe4h^J6wh@YCabAe(+NYyG*12M_XYX~PC5qyj+mSQ)@=z~bfyQrAkFi$#+Fh43c! z(Sy*X$gFxleo#!{Uae8?D%(q-p)mexyqQugP%!Y%k4)CoMtj7EvznKIAWBv`PRl$a ziKr6w51LAt&)h8kDK3Wht>yUtj1C12=a;n)Lt zk+)l{CB;&z)dQwlipLb^3D^8|vN-^6S1mV`!PH}_C_M3y@ zm&`Ivs?T(}u))>;ow|n1DtlvDj0ts<0RaYNl{xB^r)_>c}^E=Cnn*b~`lA3yDc zlTT;K|Ebep+xL@s{8svq-#^V~>KuS?hnkPw0(iet1T7xt@~5koq;F-&Ort`bS~BXp zaFly~kb#bu8r!Dt0~vea$f#g--P&P~7|LJ3$9P-&bHIqqvk|6&`({hz=U3+$2;Y3m zfPX$2+maB0Ui7LG_;EEYsVq0A7_$}9lEb8M&p}XRG-6BYm#m)&_7?ACttiI z%R4pzF($%&u+23_8Gwzr)-w_SzIAENE%b2YP*obl-CL6TAgWffQ>z^NTfYM)VgHni zamI@rLrK!v#bVZW`z(7$itB4l345}t1O>$jKocq5E5bw$ixOA94c;8e{NL#!U4J#t z^4P~HOaQ#C1-=hTac2Xf!uF+Iq0SDgwuDS{{uEz7!-8lzkOj@?MsEs|* z0~m#Z32DHfAB-H6I<2+TL(rXlebcAUyPD`G4N2$_?R;_Iq@^*8jIGT(E~_5&@Q_7Y z9$-iaI1lEWX4hrV0*T_6Ej`0?5UiYr5-C12is+M2d9=R%D~rJlb|@^snmiyS&7YfY zlQvp2_ZT%mTe(lR<%kE2XH^wS+V^kJr>i+nIb4exM}%bTa)-~qnc=_{9pM)eb}<_9 zC9or=3k209BO6>9jlQjXi-=$SJn-H6gF|d$xYWoB;Z^$>2^_sfshnd1<+LD(Bmv;4pQz3tL7KHFGT*LL1% zm`cz~sj=KAuah`#2fTs%+?E5ekoG*H>@+)5CRz1;n}0A(FflL<1_@w>NC9O71OfpC z00baB8Hm!DuY0kS4x(f8S*~~uAIi@E$kkJ#b;K)xS7J5<6bYEpaxfae(nmaK{0t=R M2=(CX6aoS#5cboEng9R* literal 0 HcmV?d00001