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 01/13] 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 8969a9f77dd9a3d49aab6f5d47e0e078b4daedb2 Mon Sep 17 00:00:00 2001 From: cruddasj Date: Mon, 18 Aug 2025 21:49:31 +0100 Subject: [PATCH 02/13] fix(NON-REQ): update REAMDE.md installation steps based on testing (#3) * fix(NON-REQ): update REAMDE.md installation steps based on testing * fix(NON-REQ): minor text update * fix(NON-REQ): minor formatting update * fix(NON-REQ): fix env template name * fix(NON-REQ): ignore env files * fix(NON-REQ): add note about certificate country names --- .gitignore | 12 +++++++++++ README.md | 32 +++++++++++++++++------------- docker/keycloak/.env.template | 21 ++++++++++++++++++++ docker/keycloak/docker-compose.yml | 10 +++++----- 4 files changed, 56 insertions(+), 19 deletions(-) create mode 100644 docker/keycloak/.env.template diff --git a/.gitignore b/.gitignore index 667aaef..681364d 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,15 @@ build/ ### VS Code ### .vscode/ + +### Development certificates ### +*.key +*.csr +*.crt +*.p12 +*.jks +*.ext +*.pem + +### Env files ### +.env \ No newline at end of file diff --git a/README.md b/README.md index 34fed40..f2e8b78 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The application uses Keycloak for authentication and authorization. Follow these cd docker ``` -2. Make sure you have the required certificates in place: +2. Make sure you have the required certificates in the `docker` directory: - `keystore.jks` - Java keystore containing the server certificate - `truststore.jks` - Java truststore containing trusted certificates - `localhost.p12` - PKCS12 keystore for client authentication @@ -31,7 +31,7 @@ The application uses Keycloak for authentication and authorization. Follow these 3. Start Keycloak and PostgreSQL using Docker Compose: ```bash - docker-compose up -d + docker compose -f keycloak/docker-compose.yml up -d ``` 4. Verify that Keycloak is running: @@ -116,7 +116,7 @@ The system requires several certificate files: ### Step-by-Step Certificate Generation -For development purposes, follow these steps to generate certificates for mTLS. All passwords used are `changeit`. +For development purposes, follow these steps to generate certificates for mTLS. All passwords used are `changeit`. When generating these certficates, for the `Country Name`, you can use the value of 'UK'. All remaining certificate fields can be left to their default values. 1. **Generate a Root CA certificate**: ```bash @@ -201,16 +201,6 @@ For development purposes, follow these steps to generate certificates for mTLS. ``` 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: @@ -267,7 +257,7 @@ For production environments, use strong, unique passwords and secure storage sol ## 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. +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. To access the administrative interface at https://localhost:8443/admin, you will need to first import your client.p12 digital certificate file into your local browser, else the request will be rejected. ### Option 1: Import the Realm Configuration (Recommended) @@ -294,6 +284,20 @@ If you prefer to set up the realm manually: - Web Origins: `+` 4. Note the client secret from the Credentials tab and update it in your application.yml if needed +### Testing mTLS connectivity: + +Once KeyCloak is running and configured, you can test mTLS connectivity using the below command: + + ```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. + ## Building and Running with Maven ### Building the Application diff --git a/docker/keycloak/.env.template b/docker/keycloak/.env.template new file mode 100644 index 0000000..74a1a01 --- /dev/null +++ b/docker/keycloak/.env.template @@ -0,0 +1,21 @@ +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 \ No newline at end of file diff --git a/docker/keycloak/docker-compose.yml b/docker/keycloak/docker-compose.yml index d2a1453..7c9a904 100644 --- a/docker/keycloak/docker-compose.yml +++ b/docker/keycloak/docker-compose.yml @@ -55,11 +55,11 @@ services: 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 + - ../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: From a06844c91901631744df73f2702aa61468b50588 Mon Sep 17 00:00:00 2001 From: Nikan Negaresh <84400913+nikan-negaresh-informed@users.noreply.github.com> Date: Tue, 19 Aug 2025 09:56:51 +0100 Subject: [PATCH 03/13] feat(DPAV-1421): Add Keycloak OpenTofu configuration and supporting modules (#4) * Add Keycloak OpenTofu configuration and supporting modules - Introduced OpenTofu configuration for managing Keycloak resources including realm, clients, client scopes, and roles. - Added reusable `federator_client` module for creating clients with optional roles and mappings. - Defined helper `Makefile` and documentation for managing workflows (init, plan, apply, validate, etc.). - Configured Keycloak S3 backend and core variables for flexible workspace management. - Enhanced JWT `aud` claim parsing in `JwtToken` model. - Updated `.gitignore` to include Terraform state and configuration files. * feat(DPAV-1421): minor formatting updates * feat(DPAV-1421): add terraform directory to gitignore * feat(DPAV-1421): remove Apache 2.0 license file --------- Co-authored-by: cruddasj --- .gitignore | 8 +- LICENSE | 201 ---------------- docker/keycloak/tofu/Make-Cmds.md | 114 +++++++++ docker/keycloak/tofu/Makefile | 39 +++ docker/keycloak/tofu/README.md | 226 ++++++++++++++++++ docker/keycloak/tofu/backend.tf | 10 + .../tofu/backends/dev-backend.tfvars | 4 + docker/keycloak/tofu/client_scopes.tf | 47 ++++ docker/keycloak/tofu/clients.tf | 74 ++++++ .../tofu/modules/federator_client/main.tf | 93 +++++++ .../tofu/modules/federator_client/outputs.tf | 9 + .../modules/federator_client/providers.tf | 9 + .../modules/federator_client/variables.tf | 131 ++++++++++ docker/keycloak/tofu/providers.tf | 8 + docker/keycloak/tofu/realm.tf | 23 ++ docker/keycloak/tofu/terraform.tfvars | 57 +++++ docker/keycloak/tofu/tfvars/dev.tfvars | 0 docker/keycloak/tofu/variables.tf | 55 +++++ .../node/management/model/jwt/JwtToken.java | 2 + 19 files changed, 908 insertions(+), 202 deletions(-) delete mode 100644 LICENSE create mode 100644 docker/keycloak/tofu/Make-Cmds.md create mode 100644 docker/keycloak/tofu/Makefile create mode 100644 docker/keycloak/tofu/README.md create mode 100644 docker/keycloak/tofu/backend.tf create mode 100644 docker/keycloak/tofu/backends/dev-backend.tfvars create mode 100644 docker/keycloak/tofu/client_scopes.tf create mode 100644 docker/keycloak/tofu/clients.tf create mode 100644 docker/keycloak/tofu/modules/federator_client/main.tf create mode 100644 docker/keycloak/tofu/modules/federator_client/outputs.tf create mode 100644 docker/keycloak/tofu/modules/federator_client/providers.tf create mode 100644 docker/keycloak/tofu/modules/federator_client/variables.tf create mode 100644 docker/keycloak/tofu/providers.tf create mode 100644 docker/keycloak/tofu/realm.tf create mode 100644 docker/keycloak/tofu/terraform.tfvars create mode 100644 docker/keycloak/tofu/tfvars/dev.tfvars create mode 100644 docker/keycloak/tofu/variables.tf diff --git a/.gitignore b/.gitignore index 681364d..9838bfb 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,10 @@ build/ *.pem ### Env files ### -.env \ No newline at end of file +.env + +#### States +.terraform/ +*.tfstate +*.tfstate.backup +*.hcl \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 261eeb9..0000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. diff --git a/docker/keycloak/tofu/Make-Cmds.md b/docker/keycloak/tofu/Make-Cmds.md new file mode 100644 index 0000000..4f0e654 --- /dev/null +++ b/docker/keycloak/tofu/Make-Cmds.md @@ -0,0 +1,114 @@ +# Keycloak OpenTofu Make Commands + +This document describes how to use the Makefile in this folder to manage Keycloak resources (realm, clients, client scopes) with OpenTofu. + +Important notes: +- Run these commands from: docker/keycloak/tofu +- Use DIR=. for this repository (the Makefile default DIR=01-global is an upstream default). +- Workspaces (e.g., dev) map to different state and tfvars files. + +--- + +## Quick start + +Initialize OpenTofu, select/create the workspace, and configure the S3 backend: + +```sh +make init WORKSPACE=dev DIR=. +``` + +If your backend file is custom, make sure it matches backends/-backend.tfvars. Example for dev: backends/dev-backend.tfvars. + +--- + +## Plan and apply + +Plan changes (loads terraform.tfvars automatically and tfvars/.tfvars if present): + +```sh +make plan WORKSPACE=dev DIR=. +``` + +Apply the last plan: + +```sh +make apply WORKSPACE=dev DIR=. +``` + +Or apply directly with auto-approve: + +```sh +make apply-auto-approve WORKSPACE=dev DIR=. +``` + +--- + +## Destroy + +Create a destroy plan and destroy resources for the selected workspace: + +```sh +make destroy-plan WORKSPACE=dev DIR=. +make destroy WORKSPACE=dev DIR=. +``` + +--- + +## Validation and formatting + +Format all files and validate configuration: + +```sh +make format +make validate WORKSPACE=dev DIR=. +``` + +Pre-check (fmt -check + validate): + +```sh +make pre-check WORKSPACE=dev DIR=. +``` + +Pre-commit convenience target (runs format and validate): + +```sh +make pre-commit WORKSPACE=dev DIR=. +``` + +--- + +## Upgrade and re-init + +If providers/modules were updated or you need a clean init: + +```sh +make init-upgrade WORKSPACE=dev DIR=. +``` + +--- + +## Variables and files + +- Backend config: backends/-backend.tfvars (e.g., backends/dev-backend.tfvars) +- Per-workspace variables: tfvars/.tfvars (e.g., tfvars/dev.tfvars) +- Default variables: terraform.tfvars + +Key variables (see variables.tf and terraform.tfvars): +- keycloak_url, keycloak_realm, keycloak_client_id, keycloak_username, keycloak_password, keycloak_client_timeout +- management_realm_name, client_access_token_lifespan_seconds +- federator_clients (module input for optional federator clients and role mappings) + +--- + +## Example workflow + +```sh +# From docker/keycloak/tofu +make init WORKSPACE=dev DIR=. +make plan WORKSPACE=dev DIR=. +make apply WORKSPACE=dev DIR=. +``` + +That's it - no AWS regional/global directories are needed here. This Makefile and commands are scoped to the Keycloak OpenTofu configuration in this folder. + + diff --git a/docker/keycloak/tofu/Makefile b/docker/keycloak/tofu/Makefile new file mode 100644 index 0000000..651752d --- /dev/null +++ b/docker/keycloak/tofu/Makefile @@ -0,0 +1,39 @@ +# Default values +WORKSPACE ?= dev +DIR ?= 01-global # Change this based on the directory you want to work with + +init: + @cd $(DIR) && tofu init -backend-config=backends/$(WORKSPACE)-backend.tfvars + @cd $(DIR) && tofu workspace select $(WORKSPACE) || tofu workspace new $(WORKSPACE) + +plan: + @cd $(DIR) && tofu plan -var-file=tfvars/$(WORKSPACE).tfvars -out=tfplan + +apply: + @cd $(DIR) && tofu apply tfplan + +apply-auto-approve: + @cd $(DIR) && tofu apply -auto-approve + +destroy-plan: + @cd $(DIR) && tofu plan -destroy -var-file=tfvars/$(WORKSPACE).tfvars + +destroy: + @cd $(DIR) && tofu destroy -var-file=tfvars/$(WORKSPACE).tfvars + +format: + tofu fmt --recursive + +validate: + @cd $(DIR) && tofu validate + +pre-check: + @cd $(DIR) && tofu fmt -check + @cd $(DIR) && tofu validate + +init-upgrade: + @cd $(DIR) && rm -rf .terraform + @cd $(DIR) && tofu init -upgrade -backend-config=backends/$(WORKSPACE)-backend.tfvars + @cd $(DIR) && tofu workspace select $(WORKSPACE) || tofu workspace new $(WORKSPACE) + +pre-commit: format validate diff --git a/docker/keycloak/tofu/README.md b/docker/keycloak/tofu/README.md new file mode 100644 index 0000000..3ea648a --- /dev/null +++ b/docker/keycloak/tofu/README.md @@ -0,0 +1,226 @@ +**Repository:** `management-node` +**Description:** `OpenTofu configuration for managing Keycloak (realm, clients, client scopes) used by the Management Node.` + +# Overview + +This directory contains OpenTofu code to provision and manage Keycloak resources for the Management Node: +- Realm definition and settings +- Application clients and roles +- Client scopes and role mappings +- Optional federator clients via a module (modules/federator_client) + +The configuration uses the Keycloak provider and an S3 backend for state (configured via backend tfvars). + +--- + +## Directory Layout + +- backend.tf - Declares the S3 backend and required providers +- providers.tf - Configures the Keycloak provider using variables +- variables.tf - Input variables used across the configuration +- realm.tf - Realm creation and base configuration +- clients.tf - Clients and role definitions +- client_scopes.tf - Client scopes and mappers +- terraform.tfvars - Default variable values for local/dev usage +- backends/ - Backend config files (e.g., dev-backend.tfvars) +- tfvars/ - Optional per-workspace tfvars files (e.g., dev.tfvars) +- modules/ - Reusable modules (e.g., federator_client) +- Makefile - Helper targets to init/plan/apply/destroy/validate + +Tip: The Makefile expects to run commands from this tofu folder and supports a WORKSPACE and DIR variable. For this repository, DIR should be set to the current directory (.). + +--- + +## Using the Makefile + +The Makefile simplifies running OpenTofu commands. + +Defaults: +- WORKSPACE=dev +- DIR=01-global (upstream default; override to . for this repo) + +Recommended to always pass DIR=. + +### 1. Setup (`make init`) +Initializes OpenTofu, selects/creates the workspace, and configures the S3 backend. + +```sh +# From docker/keycloak/tofu +make init WORKSPACE=dev DIR=. +``` + +If your backend file is named differently, adjust accordingly or rename it to match backends/-backend.tfvars. For example, ensure backends/dev-backend.tfvars exists for WORKSPACE=dev. + +### 2. Plan changes (`make plan`) +Generates an execution plan. terraform.tfvars is loaded automatically; tfvars/dev.tfvars can be used for per-workspace overrides. + +```sh +make plan WORKSPACE=dev DIR=. +``` + +### 3. Apply changes (`make apply`) +Applies the previously generated plan. + +```sh +make apply WORKSPACE=dev DIR=. +``` + +Alternatively, apply directly with auto-approve: + +```sh +make apply-auto-approve WORKSPACE=dev DIR=. +``` + +### 4. Destroy resources (`make destroy`) +Plans a destroy and destroys resources for the given workspace. + +```sh +make destroy-plan WORKSPACE=dev DIR=. +make destroy WORKSPACE=dev DIR=. +``` + +### 5. Validate and format (`make validate`, `make format`) + +```sh +make format +make validate WORKSPACE=dev DIR=. +``` + +### 6. Pre-check (`make pre-check`) +Runs fmt -check and validate. + +```sh +make pre-check WORKSPACE=dev DIR=. +``` + +--- + +## Variables +Key variables you may need to set (see variables.tf and terraform.tfvars): +- keycloak_url, keycloak_realm, keycloak_client_id, keycloak_username, keycloak_password, keycloak_client_timeout +- management_realm_name +- client_access_token_lifespan_seconds +- federator_clients (structured list for module-driven client creation and role mappings) + +These can be provided via terraform.tfvars, tfvars/.tfvars, or -var/-var-file flags. + +--- + +## Example Workflow + +```sh +cd docker/keycloak/tofu +make init WORKSPACE=dev DIR=. +make format +make validate WORKSPACE=dev DIR=. +make plan WORKSPACE=dev DIR=. +make apply WORKSPACE=dev DIR=. +``` + +--- + +## Using a local backend (no S3) on your machine +If you want to try this locally without configuring an S3 bucket, you can switch the backend from S3 to local. There are two simple approaches: + +### Option A: Use a local backend block +Edit docker/keycloak/tofu/backend.tf and change the backend block to local: + +```hcl +terraform { + required_version = ">= 1.6.0" + required_providers { + keycloak = { + source = "keycloak/keycloak" + version = "~> 5.4" + } + } + backend "local" { + # The default path is ./terraform.tfstate + path = "terraform.tfstate" + } +} +``` + +Then run OpenTofu directly (skip the Makefile init which assumes S3): + +```sh +cd docker/keycloak/tofu +# Initialize with local backend (no -backend-config needed) +tofu init +# Create/select your workspace +tofu workspace select dev || tofu workspace new dev +# Plan and apply +tofu plan -var-file=tfvars/dev.tfvars -out=tfplan +tofu apply tfplan +``` + +### Option B: Comment out the S3 backend +Alternatively, simply comment out the S3 backend line in backend.tf so there is no backend block. OpenTofu defaults to the local backend in this case: + +Before: +```hcl +terraform { + required_version = ">= 1.6.0" + required_providers { + keycloak = { + source = "keycloak/keycloak" + version = "~> 5.4" + } + } + backend "s3" {} +} +``` + +After (S3 backend commented out): +```hcl +terraform { + required_version = ">= 1.6.0" + required_providers { + keycloak = { + source = "keycloak/keycloak" + version = "~> 5.4" + } + } + # backend "s3" {} +} +``` + +Then initialize and apply as in Option A: + +```sh +cd docker/keycloak/tofu +tofu init +tofu workspace select dev || tofu workspace new dev +tofu plan -var-file=tfvars/dev.tfvars -out=tfplan +tofu apply tfplan +``` + +Notes for local usage: +- The state file terraform.tfstate will be created next to backend.tf (and is already ignored by .gitignore). +- If you want to keep using the Makefile for plan/apply, you can: + - Run tofu init manually as shown above (so it uses local backend), and then + - Use the Makefile for subsequent targets, skipping make init, e.g.: + ```sh + cd docker/keycloak/tofu + tofu init + tofu workspace select dev || tofu workspace new dev + make plan WORKSPACE=dev DIR=. + make apply WORKSPACE=dev DIR=. + ``` +- To switch back to S3 later, restore the backend "s3" {} block and run: + ```sh + cd docker/keycloak/tofu + tofu init -reconfigure -backend-config=backends/dev-backend.tfvars + ``` + +--- + +## Notes +- Backend: The backend is defined as S3 in backend.tf and is configured via backends/-backend.tfvars. Update bucket/key/region to match your environment. +- Provider auth: providers.tf uses admin credentials (admin-cli) by default for local dev. For production, configure a service account and secure secrets appropriately. +- Docker: When using docker-compose Keycloak locally (default at http://localhost:8080), the sample terraform.tfvars should work once admin credentials are set to admin/password. + +--- + +## Contributors +Thanks to all contributors of this repository: https://github.com/National-Digital-Twin/management-node/graphs/contributors diff --git a/docker/keycloak/tofu/backend.tf b/docker/keycloak/tofu/backend.tf new file mode 100644 index 0000000..62cb814 --- /dev/null +++ b/docker/keycloak/tofu/backend.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.6.0" + required_providers { + keycloak = { + source = "keycloak/keycloak" + version = "~> 5.4" + } + } + backend "s3" {} +} \ No newline at end of file diff --git a/docker/keycloak/tofu/backends/dev-backend.tfvars b/docker/keycloak/tofu/backends/dev-backend.tfvars new file mode 100644 index 0000000..0034bc6 --- /dev/null +++ b/docker/keycloak/tofu/backends/dev-backend.tfvars @@ -0,0 +1,4 @@ +bucket = "5371-2494-4113-state" +key = "keycloak/01-base/dev/terraform.tfstate" +region = "eu-west-2" +encrypt = true \ No newline at end of file diff --git a/docker/keycloak/tofu/client_scopes.tf b/docker/keycloak/tofu/client_scopes.tf new file mode 100644 index 0000000..e1a13aa --- /dev/null +++ b/docker/keycloak/tofu/client_scopes.tf @@ -0,0 +1,47 @@ +# Defines custom OpenID client scopes for the Management Node realm +# These scopes are referenced by modules/clients via their names + +resource "keycloak_openid_client_scope" "federator_consumer" { + realm_id = keycloak_realm.management-node.id + name = "FEDERATOR_CONSUMER" + description = "Client scope for Federator consumer" +} + +resource "keycloak_openid_client_scope" "federator_producer" { + realm_id = keycloak_realm.management-node.id + name = "FEDERATOR_PRODUCER" + description = "Client scope for Federator producer" +} + +# Scope that adds management-node audience and exposes its client roles in tokens +resource "keycloak_openid_client_scope" "management_node_access" { + realm_id = keycloak_realm.management-node.id + name = "MANAGEMENT_NODE_ACCESS" + description = "Adds management-node audience and maps its client roles" +} + +# Add audience mapper to include management-node in the 'aud' claim for tokens using this scope +resource "keycloak_openid_audience_protocol_mapper" "management_node_aud" { + realm_id = keycloak_realm.management-node.id + client_scope_id = keycloak_openid_client_scope.management_node_access.id + name = "aud-management-node" + + included_client_audience = "management-node" + + add_to_access_token = true + add_to_id_token = false +} + +# Map client roles from management-node into resource_access.management-node.roles +resource "keycloak_openid_user_client_role_protocol_mapper" "management_node_roles" { + realm_id = keycloak_realm.management-node.id + client_scope_id = keycloak_openid_client_scope.management_node_access.id + name = "roles-management-node" + + # client whose roles will be added to the token + + claim_name = "resource_access.management-node.roles" + add_to_access_token = true + add_to_id_token = false + multivalued = true +} diff --git a/docker/keycloak/tofu/clients.tf b/docker/keycloak/tofu/clients.tf new file mode 100644 index 0000000..edc3160 --- /dev/null +++ b/docker/keycloak/tofu/clients.tf @@ -0,0 +1,74 @@ +resource "keycloak_openid_client" "management_node" { + realm_id = keycloak_realm.management-node.id + client_id = "management-node" + name = "Management Node" + description = "Management Node Client" + enabled = true + access_type = "CONFIDENTIAL" + standard_flow_enabled = false # disable browser-based auth + direct_access_grants_enabled = false + service_accounts_enabled = true # allow use of client credentials + + # Per-client JWT access token lifespan (seconds) + access_token_lifespan = var.client_access_token_lifespan_seconds +} + + +# Create custom client roles for the management-node client +resource "keycloak_role" "access_consumer_configurations" { + realm_id = keycloak_realm.management-node.id + client_id = keycloak_openid_client.management_node.id + name = "access_consumer_configurations" + description = "Allows access to consumer configuration resources" +} + +resource "keycloak_role" "access_producer_configurations" { + realm_id = keycloak_realm.management-node.id + client_id = keycloak_openid_client.management_node.id + name = "access_producer_configurations" + description = "Allows access to producer configuration resources" +} + +# Configure clients from federator_clients variable +module "federator_client" { + source = "./modules/federator_client" + + for_each = { for c in var.federator_clients : c.client => c } + + client_id = each.value.client + realm_id = keycloak_realm.management-node.id + + # Ensure base client and its roles exist before mapping + depends_on = [ + keycloak_openid_client.management_node, + keycloak_role.access_consumer_configurations, + keycloak_role.access_producer_configurations, + ] + + # Token lifespan for this client + client_access_token_lifespan_seconds = var.client_access_token_lifespan_seconds + + # Reuse the same default client scopes as the other federator clients + default_client_scopes = [ + "FEDERATOR_CONSUMER", + "FEDERATOR_PRODUCER", + "MANAGEMENT_NODE_ACCESS", + ] + + # Create roles under this client + custom_roles = [ + for r in lookup(each.value, "roles", []) : { + name = r + } + ] + + # Assign mapped roles from other clients to this client's service account + service_account_role_ids = flatten([ + for m in lookup(each.value, "mapped_client_roles", []) : [ + for role_name in m.roles : { + name = role_name + from_client = m.client + } + ] + ]) +} \ No newline at end of file diff --git a/docker/keycloak/tofu/modules/federator_client/main.tf b/docker/keycloak/tofu/modules/federator_client/main.tf new file mode 100644 index 0000000..f85decc --- /dev/null +++ b/docker/keycloak/tofu/modules/federator_client/main.tf @@ -0,0 +1,93 @@ +resource "keycloak_openid_client" "this" { + realm_id = var.realm_id + client_id = var.client_id + name = coalesce(var.name, var.client_id) + description = var.description + enabled = var.enabled + access_type = var.access_type + standard_flow_enabled = var.standard_flow_enabled + implicit_flow_enabled = var.implicit_flow_enabled + direct_access_grants_enabled = var.direct_access_grants_enabled + service_accounts_enabled = var.service_accounts_enabled + + # Per-client JWT access token lifespan (seconds) + access_token_lifespan = var.client_access_token_lifespan_seconds + + # Optional toggles most people like off in machine clients + consent_required = var.consent_required + backchannel_logout_session_required = var.backchannel_logout_session_required + backchannel_logout_url = var.backchannel_logout_url + + client_authenticator_type = var.client_authenticator_type + + extra_config = { + "x509.subjectdn" = var.x509_subject_dn + "x509.allow.regex.pattern.comparison" = tostring(var.x509_allow_regex_pattern_comparison) + } +} + +# Attach default client scopes if provided +resource "keycloak_openid_client_default_scopes" "this" { + count = length(var.default_client_scopes) > 0 ? 1 : 0 + realm_id = var.realm_id + client_id = keycloak_openid_client.this.id + default_scopes = var.default_client_scopes +} + +# Attach optional client scopes if provided +resource "keycloak_openid_client_optional_scopes" "this" { + count = length(var.optional_client_scopes) > 0 ? 1 : 0 + realm_id = var.realm_id + client_id = keycloak_openid_client.this.id + optional_scopes = var.optional_client_scopes +} + +# Custom client roles (scoped to this client) +resource "keycloak_role" "custom_roles" { + for_each = { for r in var.custom_roles : r.name => r } + realm_id = var.realm_id + client_id = keycloak_openid_client.this.id + name = each.value.name + description = try(each.value.description, null) +} + +# Resolve container client UUIDs for the provided roles +# Build a set of unique source client_ids (strings) we need to resolve +locals { + role_source_clients = toset([for r in var.service_account_role_ids : r.from_client]) +} + +data "keycloak_openid_client" "role_containers" { + for_each = local.role_source_clients + realm_id = var.realm_id + client_id = each.key +} + +# Assign provided roles to this client's service account (if any provided) +resource "keycloak_openid_client_service_account_role" "service_account_roles" { + # Use only input-derived, stable keys for for_each to avoid plan-time unknowns + for_each = { for r in var.service_account_role_ids : "${r.from_client}:${r.name}" => r } + + realm_id = var.realm_id + # IMPORTANT: this client_id must be the container (owner) of the role) + # Resolve the container client's UUID here (arguments may be unknown at plan time, which is OK) + client_id = data.keycloak_openid_client.role_containers[each.value.from_client].id + service_account_user_id = keycloak_openid_client.this.service_account_user_id + + # Provider expects 'role' to be the role NAME + role = each.value.name +} + +# Optionally assign custom roles defined on this client to its own service account +# This ensures roles are not only created under the client, but are also granted to the +# service account so that the corresponding audience is applied in tokens. +resource "keycloak_openid_client_service_account_role" "assign_custom_roles_to_sa" { + for_each = var.assign_roles_to_service_account && var.service_accounts_enabled ? keycloak_role.custom_roles : {} + + realm_id = var.realm_id + client_id = keycloak_openid_client.this.id + service_account_user_id = keycloak_openid_client.this.service_account_user_id + role = each.key +} + + diff --git a/docker/keycloak/tofu/modules/federator_client/outputs.tf b/docker/keycloak/tofu/modules/federator_client/outputs.tf new file mode 100644 index 0000000..03ee6d9 --- /dev/null +++ b/docker/keycloak/tofu/modules/federator_client/outputs.tf @@ -0,0 +1,9 @@ +output "client_uuid" { + description = "UUID of the created Keycloak client (container client_id for role assignments)" + value = keycloak_openid_client.this.id +} + +output "custom_role_names" { + description = "List of custom role names created under this client (if any)" + value = keys(keycloak_role.custom_roles) +} \ No newline at end of file diff --git a/docker/keycloak/tofu/modules/federator_client/providers.tf b/docker/keycloak/tofu/modules/federator_client/providers.tf new file mode 100644 index 0000000..6d96d96 --- /dev/null +++ b/docker/keycloak/tofu/modules/federator_client/providers.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.6.0" + required_providers { + keycloak = { + source = "keycloak/keycloak" + version = "~> 5.4" + } + } +} diff --git a/docker/keycloak/tofu/modules/federator_client/variables.tf b/docker/keycloak/tofu/modules/federator_client/variables.tf new file mode 100644 index 0000000..8787999 --- /dev/null +++ b/docker/keycloak/tofu/modules/federator_client/variables.tf @@ -0,0 +1,131 @@ +variable "realm_id" { + description = "Target realm where the client will be created" + type = string +} + +variable "client_id" { + description = "OIDC client_id" + type = string +} + +variable "name" { + description = "Human-friendly name (defaults to client_id)" + type = string + default = null +} + +variable "description" { + type = string + default = null +} + +variable "enabled" { + type = bool + default = true +} + +variable "access_type" { + description = "CONFIDENTIAL | PUBLIC | BEARER-ONLY" + type = string + default = "CONFIDENTIAL" +} + +variable "standard_flow_enabled" { + # auth code + type = bool + default = false +} + +variable "implicit_flow_enabled" { + type = bool + default = false +} + +variable "direct_access_grants_enabled" { + # ROPC + type = bool + default = false +} + +variable "service_accounts_enabled" { + type = bool + default = true +} + +variable "consent_required" { + type = bool + default = false +} + +variable "backchannel_logout_session_required" { + type = bool + default = false +} +variable "backchannel_logout_url" { + type = string + default = null +} + +variable "custom_roles" { + description = "List of custom client roles to create on this client" + type = list(object({ + name = string + description = optional(string) + })) + default = [] +} + +variable "assign_roles_to_service_account" { + description = "If true, assign custom + extra roles to the service account" + type = bool + default = true +} + +# X.509 client authentication settings +variable "client_authenticator_type" { + description = "Client authenticator type to use for this client (e.g., client-secret, client-x509)" + type = string + default = "client-x509" +} + +variable "x509_subject_dn" { + description = "Expected Subject DN for TLS client authentication. Supports regex when x509_allow_regex_pattern_comparison is true." + type = string + default = "(.*?)(?:$)" +} + +variable "x509_allow_regex_pattern_comparison" { + description = "Whether to allow regex pattern comparison for x509.subjectdn. Keycloak attribute: x509.allow.regex.pattern.comparison" + type = bool + default = true +} + +# Client scopes to be attached to this client +variable "default_client_scopes" { + description = "List of existing client scopes to attach as default scopes to this client" + type = list(string) + default = [] +} + +variable "optional_client_scopes" { + description = "List of existing client scopes to attach as optional scopes to this client" + type = list(string) + default = [] +} + +# Roles (with container client reference) to assign to this client's service account user +variable "service_account_role_ids" { + description = "List of roles to assign to the client's service account. Each item must contain the role name and the source client identifier (client_id string) that owns the role. The module will resolve it to a UUID." + type = list(object({ + name = string # role NAME + from_client = string # source client_id (string, e.g., 'management-node' or another client_id) + })) + default = [] +} + +# Token settings +variable "client_access_token_lifespan_seconds" { + description = "Access token lifespan for this client (in seconds). Default 30 minutes (1800)." + type = number + default = 1800 +} diff --git a/docker/keycloak/tofu/providers.tf b/docker/keycloak/tofu/providers.tf new file mode 100644 index 0000000..e525fde --- /dev/null +++ b/docker/keycloak/tofu/providers.tf @@ -0,0 +1,8 @@ +provider "keycloak" { + url = var.keycloak_url + realm = var.keycloak_realm # manage realms from master + client_id = var.keycloak_client_id + username = var.keycloak_username + password = var.keycloak_password + client_timeout = var.keycloak_client_timeout +} diff --git a/docker/keycloak/tofu/realm.tf b/docker/keycloak/tofu/realm.tf new file mode 100644 index 0000000..df98101 --- /dev/null +++ b/docker/keycloak/tofu/realm.tf @@ -0,0 +1,23 @@ +resource "keycloak_realm" "management-node" { + realm = var.management_realm_name + display_name = "Management Node Realm" + enabled = true + + # Common login toggles + login_with_email_allowed = false + registration_allowed = false + reset_password_allowed = false + + # Optional: configure password policy + password_policy = "hashIterations(27500) and length(12) and digits(1) and specialChars(1)" +} + +# Manage realm default roles so that built-in account roles are not assigned by default +resource "keycloak_default_roles" "management_node_defaults" { + realm_id = keycloak_realm.management-node.id + + # Do not assign any realm-level default roles to new users + # This effectively prevents Keycloak from granting the composite 'default-roles-' + # which includes built-in client roles like 'manage-account' and 'view-profile'. + default_roles = [] +} diff --git a/docker/keycloak/tofu/terraform.tfvars b/docker/keycloak/tofu/terraform.tfvars new file mode 100644 index 0000000..74d51a2 --- /dev/null +++ b/docker/keycloak/tofu/terraform.tfvars @@ -0,0 +1,57 @@ +keycloak_url = "http://localhost:8080" +keycloak_realm = "master" +keycloak_client_id = "admin-cli" +keycloak_username = "admin" +keycloak_password = "password" +keycloak_client_timeout = 30 + +management_realm_name = "mng-node" + +# JWT access token lifespan for clients (in seconds). Default is 1800 (30 minutes) +client_access_token_lifespan_seconds = 1800 + +# Structured federator clients configuration +federator_clients = [ + + { + client = "FEDERATOR_ENV" + roles = ["FloodRiskMapZones"] + mapped_client_roles = [ + { + client = "management-node" + roles = ["access_producer_configurations", "access_consumer_configurations"] + } + ] + }, + { + client = "FEDERATOR_BCC" + roles = ["PendingPlanningApplications"] + mapped_client_roles = [ + { + client = "management-node" + roles = ["access_producer_configurations", "access_consumer_configurations"] + } + ] + }, + { + client = "FEDERATOR_HEG" + roles = ["BrownfieldLandAvailability"] + mapped_client_roles = [ + { + client = "management-node" + roles = ["access_producer_configurations", "access_consumer_configurations"] + } + ] + }, + { + client = "MANAGEMENT_NODE_CLIENT" + roles = [] + mapped_client_roles = [ + { + client = "management-node" + roles = ["access_producer_configurations", "access_consumer_configurations"] + } + ] + } +] + diff --git a/docker/keycloak/tofu/tfvars/dev.tfvars b/docker/keycloak/tofu/tfvars/dev.tfvars new file mode 100644 index 0000000..e69de29 diff --git a/docker/keycloak/tofu/variables.tf b/docker/keycloak/tofu/variables.tf new file mode 100644 index 0000000..f366be3 --- /dev/null +++ b/docker/keycloak/tofu/variables.tf @@ -0,0 +1,55 @@ +variable "keycloak_url" { + description = "Keycloak base URL" + type = string +} + +variable "keycloak_realm" { + description = "Realm to manage (typically 'master' for administrative operations)" + type = string +} + +variable "keycloak_client_id" { + description = "Keycloak client ID used for authentication" + type = string +} + +variable "keycloak_username" { + description = "Admin username for Keycloak" + type = string +} + +variable "keycloak_password" { + description = "Admin password for Keycloak" + type = string + sensitive = true +} + +variable "keycloak_client_timeout" { + description = "Timeout in seconds for Keycloak provider client requests" + type = number +} + +variable "management_realm_name" { + description = "Name of the Keycloak realm to create/manage" + type = string +} + +variable "client_access_token_lifespan_seconds" { + description = "Access token lifespan for clients (in seconds). Default 30 minutes (1800)." + type = number + default = 1800 +} + +variable "federator_clients" { + description = "List of federator clients to create with their own roles and mapped roles from other clients" + type = list(object({ + client = string + roles = optional(list(string), []) + mapped_client_roles = optional(list(object({ + client = string + roles = list(string) + })), []) + })) + default = [] +} + 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 index 72c925f..157fd32 100644 --- 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 @@ -4,6 +4,7 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import com.fasterxml.jackson.annotation.JsonFormat; import java.util.List; import java.util.Map; @@ -21,6 +22,7 @@ public class JwtToken { private Long iat; private String jti; private String iss; + @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) private List aud; private String sub; private String typ; From 7005c4f294d61b761d06d0f37f59894d2a0d0591 Mon Sep 17 00:00:00 2001 From: cruddasj Date: Wed, 27 Aug 2025 15:05:59 +0100 Subject: [PATCH 04/13] feature(OSPO): add inner-source licensing materials (#5) --- ACKNOWLEDGEMENTS.md | 29 +++++++++++ CHANGELOG.md | 71 +++++++++++++++++++++++++ CODE_OF_CONDUCT.md | 81 +++++++++++++++++++++++++++++ CONTRIBUTING.md | 123 ++++++++++++++++++++++++++++++++++++++++++++ LICENSE.md | 96 ++++++++++++++++++++++++++++++++++ MAINTAINERS.md | 64 +++++++++++++++++++++++ README.md | 57 ++++++++++++++++++-- SECURITY.md | 73 ++++++++++++++++++++++++++ 8 files changed, 591 insertions(+), 3 deletions(-) create mode 100644 ACKNOWLEDGEMENTS.md create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.md create mode 100644 MAINTAINERS.md create mode 100644 SECURITY.md diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md new file mode 100644 index 0000000..69eef92 --- /dev/null +++ b/ACKNOWLEDGEMENTS.md @@ -0,0 +1,29 @@ +# Acknowledgements + +**Repository:** `management-node` +**Description:** `Recognises suppliers, partner organisations, and other contributors to the repository's development.` + +The National Digital Twin Programme (NDTP) would like to acknowledge the contributions of various organisations and individuals who have supported the development of this repository. + +## Organisational contributions + +Over time, the following organisations have provided technical expertise, development support, and domain knowledge that have contributed to the evolution of this project: + +- [Informed Solutions](https://informed.com) + +We are grateful for the collaboration that has helped shape this repository. + +## Individual contributions + +For a list of individual contributors who have made direct commits to this repository, see GitHub’s auto-generated contributor insights: [Contributors](../../../graphs/contributors). + +--- + +**Note:** This acknowledgment does not confer any legal rights, ownership, or imply ongoing involvement by any of the named organisations or individuals. All contributions are made in accordance with the repository’s licensing terms. + +© 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 NDTP InnerSource Licence – Version 1.0. + +For full licensing terms, see [LICENSE.md](LICENSE.md). + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..54e5df1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,71 @@ +# Changelog + +**Repository:** `management-node` +**Description:** `Tracks all notable changes, version history, and roadmap toward 1.0.0 following Semantic Versioning.` + +All notable changes to this repository will be documented in this file. + +This project follows **Semantic Versioning (SemVer)** ([semver.org](https://semver.org/)), using the format: + + +`[MAJOR].[MINOR].[PATCH]` +- **MAJOR** (`X.0.0`) – Incompatible API/feature changes that break backward compatibility. +- **MINOR** (`0.X.0`) – Backward-compatible new features, enhancements, or functionality changes. +- **PATCH** (`0.0.X`) – Backward-compatible bug fixes, security updates, or minor corrections. +- **Pre-release versions** – Use suffixes such as `-alpha`, `-beta`, `-rc.1` (e.g., `2.1.0-beta.1`). +- **Build metadata** – If needed, use `+build` (e.g., `2.1.0+20250314`). + +--- + +## [Unreleased] + +### Added +- Placeholder for upcoming features and enhancements. + +### Fixed +- Placeholder for bug fixes and security updates. + +### Changed +- Placeholder for changes to existing functionality. + +--- + +## Future Roadmap to `1.0.0` + +The `0.90.x` series is part of NDTP’s **pre-stable development cycle**, meaning: +- **Minor versions (`0.91.0`, `0.92.0`...) introduce features and improvements** leading to a stable `1.0.0`. +- **Patch versions (`0.90.1`, `0.90.2`...) contain only bug fixes and security updates**. +- **Backward compatibility is NOT guaranteed until `1.0.0`**, though NDTP aims to minimise breaking changes. + +Once `1.0.0` is reached, future versions will follow **strict SemVer rules**. + +--- + +## Versioning Policy + +1. **MAJOR updates (`X.0.0`)** – Typically introduce breaking changes that require users to modify their code or configurations. +- **Breaking changes (default rule)**: Any backward-incompatible modifications require a major version bump. +- **Non-breaking major updates (exceptional cases)**: A major version may also be incremented if the update represents a significant milestone, such as a shift in governance, a long-term stability commitment, or substantial new functionality that redefines the project’s scope. +2. **MINOR updates (`0.X.0`)** – New functionality that is backward-compatible. +3. **PATCH updates (`0.0.X`)** – Bug fixes, performance improvements, or security patches. +4. **Dependency updates** – A **major dependency upgrade** that introduces breaking changes should trigger a **MAJOR** version bump (once at `1.0.0`). + +--- + +## How to Update This Changelog + +1. When making changes, update this file under the **Unreleased** section. +2. Before a new release, move changes from **Unreleased** to a new dated section with a version number. +3. Follow **Semantic Versioning** rules to categorise changes correctly. +4. If pre-release versions are used, clearly mark them as `-alpha`, `-beta`, or `-rc.X`. + +--- + +**Maintained by the National Digital Twin Programme (NDTP).** + +© 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 NDTP InnerSource Licence – Version 1.0. + +For full licensing terms, see [LICENSE.md](LICENSE.md). + diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..ec0ded5 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,81 @@ +# Code of Conduct + +**Repository:** `management-node` +**Description:** `Defines expected behaviors, rules, and the enforcement process to ensure professional engagement.` + +## Introduction + +The National Digital Twin Programme (NDTP) is committed to fostering an open, inclusive, and professional environment in all its repositories. +This Code of Conduct outlines the expectations for behaviour when engaging with NDTP repositories, including issue reporting, documentation feedback, +and discussions with repository maintainers. + +By participating in this repository, you agree to follow this Code of Conduct. + +--- + +## Expected Behaviour + +All contributors, maintainers, and public users are expected to: + +- **Be respectful and professional** – Treat others with courtesy and professionalism. +- **Communicate constructively** – Offer feedback that is clear, helpful, and focused on improving the repository. +- **Engage in a welcoming manner** – Encourage participation and provide a positive experience for all users. +- **Provide relevant and clear information** – When submitting issues or feedback, be specific and include details that help maintainers understand the request. + +--- + +## Unacceptable Behaviour + +The following behaviour will not be tolerated: + +- **Harassment, discrimination, or personal attacks** – Any form of offensive behaviour towards individuals or groups. +- **Trolling, disruptive comments, or inflammatory language** – Intentionally provoking arguments or making non-constructive comments. +- **Excessive demands or unrealistic expectations of maintainers** – This includes repeated requests for prioritisation outside of programme priorities. +- **Spamming or promotional content** – Off-topic discussions unrelated to the repository's purpose. +- **Disclosing sensitive information** – Sharing security vulnerabilities or confidential details outside of the responsible disclosure process. + +--- + +## Reporting Concerns + +If you believe someone is violating this Code of Conduct, please report it by following these steps: + +1. **For general issues** – Raise a concern with the repository maintainers by emailing ndtp@businessandtrade.gov.uk. +2. **For security-related concerns** – Follow the responsible disclosure process outlined in [SECURITY.md](./SECURITY.md). +3. **For incidents requiring escalation** – NDTP reserves the right to take appropriate action, including restricting access to contributors who violate this policy. + +All reports will be reviewed confidentially, and NDTP will take appropriate action to address the issue. + +--- + +## Enforcement + +Violations of this Code of Conduct may result in: + +- A formal warning +- Temporary suspension from participation +- Permanent exclusion from engaging with NDTP repositories + +Decisions on enforcement are made at NDTP’s discretion. + +--- + +## Scope + +This Code of Conduct applies to all interactions in NDTP repositories, including but not limited to: + +- Issue tracking and reporting +- Documentation suggestions and feedback +- Discussions with maintainers +- Any other interactions in public NDTP projects + +--- + +**Maintained by the National Digital Twin Programme (NDTP).** + +© 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 NDTP InnerSource Licence – Version 1.0. + +For full licensing terms, see [LICENSE.md](LICENSE.md). + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ff7b98d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,123 @@ +# Contribution Guidelines + +**Repository:** `management-node` +**Description:** `Guidelines for issue reporting, documentation suggestions, and NDTP’s controlled contribution model.` + +Thank you for your interest in this repository. + +The National Digital Twin Programme (NDTP) develops and maintains this repository in collaboration with suppliers and partner organisations, including other parts of government and their suppliers. + +NDTP follows a **Cathedral open-source governance model** where code may be made **publicly available** under open-source licences, and collaboration is invited from **approved partners**. Contributions from the general public are not currently accepted, but **feedback, issue reporting, and documentation suggestions are encouraged**. + +If you want to see which suppliers and organisations have contributed to this repository in the past, refer to [ACKNOWLEDGEMENTS.md](./ACKNOWLEDGEMENTS.md) and the GitHub contributor insights page at [Contributors](../../graphs/contributors). + +--- + +## How You Can Contribute + +Public users and NDTP partners are encouraged to engage in the following ways: + +- **Reporting bugs and issues** – If you find a problem, please open a GitHub issue. +- **Suggesting documentation improvements** – Propose clarifications or additions to the existing documentation. +- **Providing structured feedback** – If you have suggestions for improvements, let us know via GitHub Issues. + +While we review all input, NDTP prioritises development based on programme goals, supplier development cycles, and strategic objectives. + +NDTP does not currently accept **public pull requests (PRs) or direct code contributions** to this repository. Contributions are limited to **approved suppliers and partner organisations** under formal agreements. + +For details on repository maintainers and how to contact them, refer to [MAINTAINERS.md](./MAINTAINERS.md). + +--- + +## Reporting Issues + +If you encounter a bug, error, or inconsistency, please follow these steps: + +1. Check for an existing issue under [Issues](../../issues). +2. Open a new issue if no one has reported it yet. Use one of the provided issue templates. +3. Provide a clear, detailed description of the issue, including steps to reproduce it if applicable. +4. Label the issue appropriately (bug, documentation, enhancement, etc.). + +For security-related issues, do not submit a public issue. Instead, follow our [Responsible Disclosure process](./SECURITY.md). + +--- + +## Documentation Feedback + +If you find an error in the documentation, need more clarity, or have suggestions for additional documentation, you can: + +1. Open a GitHub issue under the `documentation` label. +2. Describe the improvement you are suggesting, including references to existing documentation where applicable. +3. Submit structured feedback – specific examples help us make updates faster. + +We prioritise documentation updates based on user impact and alignment with programme goals. + +--- + +## NDTP's Approach to Open-Source Development + +- **Development is led by approved suppliers and partners** who have been engaged through a formal process. +- **We welcome feedback and ideas**, but implementation is subject to programme priorities. + +To see what we’re working on, check out our [Project Roadmap](../../projects). If no roadmap is currently available, please note that it is being actively developed and will be published in due course. + +--- + +## Branching Strategy + +This repository follows a **GitFlow-based branching model** to manage development efficiently. Key conventions include: + +- **Main Branch (`main`)**: The stable, production-ready branch. Only tested and approved changes are merged here. +- **Develop Branch (`develop`)**: The integration branch where features and fixes are merged before reaching `main`. +- **Feature Branches (`feature/*`)**: Used for new developments. Named based on functionality, e.g., `feature/new-auth-method`. +- **Bugfix Branches (`bugfix/*`)**: Address minor issues in `develop` before release. +- **Release Branches (`release/*`)**: Used to prepare a new stable release, ensuring final testing and versioning updates. +- **Hotfix Branches (`hotfix/*`)**: Critical fixes applied directly to `main` and merged back into `develop`. + +For more details, refer to [GitFlow Workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow). + +--- + +## Pull Request Policy + +To maintain high-quality contributions, NDTP enforces the following **minimum pull request (PR) requirements** for approved contributors: + +- **All PRs must be reviewed by at least one maintainer** before merging. +- **PRs should reference a corresponding issue** where applicable. +- **Code changes must include relevant tests** to ensure stability. +- **Commit messages should follow best practices**, including referencing issue numbers when relevant. +- **Documentation updates should accompany PRs that impact functionality.** +- **PRs should use "squash and merge" as the preferred merge strategy**, ensuring a clean history. +- **Feature and bugfix branches should be deleted after merge** to keep the repository tidy. +- **Force pushing to the `main` branch is strictly prohibited** to protect repository integrity. +- **CI builds must pass before merging** to enforce basic validation checks. + +--- + +## Contribution Licensing + +By submitting feedback, documentation suggestions, or issue reports, you acknowledge that any resulting changes will be licensed under the same open-source terms as this repository: + +- Code contributions (if ever accepted) will be licensed under Apache 2.0. +- Documentation updates will be licensed under OGL v3.0. + +For supplier-contracted development, NDTP ensures that all contributions align with Crown Copyright and public sector open-source standards. + +--- + +## Repository Maintainers + +For details on who maintains this repository and how to contact them, refer to [MAINTAINERS.md](./MAINTAINERS.md). + +NDTP repository maintainers review reported issues, evaluate documentation suggestions, and oversee ongoing development. + +--- + +**Maintained by the National Digital Twin Programme (NDTP).** + +© 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 NDTP InnerSource Licence – Version 1.0. + +For full licensing terms, see [LICENSE.md](LICENSE.md). + diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..c334957 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,96 @@ +# NDTP InnerSource License + +**Repository:** `management-node` +**Description:** `Defines the licensing terms for the source code in this repository.` + +--- + +## Version + +**NDTP InnerSource License – Version 1.0** +**Issued by:** National Digital Twin Programme (NDTP) +**Effective Date:** 9 July 2025 + +--- + +## Copyright + +© Crown Copyright 2025. +This work has been developed by the **National Digital Twin Programme (NDTP)** and is legally attributed to the **Department for Business and Trade (UK)** as the governing entity. + +This repository is **not open source**. +Its contents are licensed under the terms of this **NDTP InnerSource License**, unless and until it is formally published under an approved open source licence by the NDTP Management Team. + +--- + +## 1. Purpose + +This repository supports InnerSource development practices within the NDTP. It enables collaborative development by internal teams and authorised suppliers, in a controlled and non-public environment. + +--- + +## 2. Licensing Status + +This work is **not licensed under an open source licence**. +It must not be published, distributed, sublicensed, or shared externally without the **explicit, written approval** of the NDTP Management Team. + +> The NDTP InnerSource Licence permits internal collaboration only. No part of this repository may be used or disclosed beyond the authorised delivery context. + +--- + +## 3. Intellectual Property + +All rights, including intellectual property rights in this code and associated materials, are owned by the NDTP. + +Where contributions are made by suppliers or delivery partners, those contributions are accepted on the basis that **full intellectual property rights** belong to the Crown under the terms of their contract. + +--- + +## 4. Permitted Use + +You may: + +- View, use, and modify the code as required to fulfil your responsibilities under the NDTP. +- Collaborate within authorised NDTP teams and with approved suppliers under existing contracts. + +You may not: + +- Share, publish, or release this repository publicly. +- Fork, clone, or redistribute this code outside approved NDTP channels. +- Apply any license other than the NDTP InnerSource License to this repository or its contents, unless instructed by the NDTP Management Team. + +--- + +## 5. Future Publication + +At the discretion of the NDTP Management Team, this repository may later be designated for release under an approved open source licence. + +Any such designation must follow NDTP's internal governance processes. + +> Until such designation is explicitly made and executed, this repository remains **confidential and proprietary**. + +--- + +## 6. Enforcement + +Any unauthorised disclosure, publication, or redistribution of this repository or its contents may: + +- Constitute a breach of contract +- Trigger formal investigation +- Result in legal, disciplinary, or commercial action, including revocation of access rights + +All actions will be escalated to the NDTP Management Team for appropriate handling. + +--- + +## Contact + +For all enquiries regarding licensing, publication status, or contributor rights, please contact: + +**NDTP Management Team** +Department for Business and Trade (UK) +NDTP@BUSINESSANDTRADE.GOV.UK + +--- + +**End of NDTP InnerSource Licence – Version 1.0** diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000..a3a1ba6 --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,64 @@ +# Maintainers + +**Repository:** `management-node` +**Description:** `Lists maintainers responsible for reviewing issues, security, and documentation updates.` + +## Introduction + +This repository is maintained by the **National Digital Twin Programme (NDTP)** in collaboration with contracted suppliers and partner organisations. + +Maintainers are responsible for reviewing issues, evaluating documentation suggestions, and overseeing +supplier-led development. + +If you need to report a problem, suggest improvements, or seek guidance on using this repository, please refer to the contacts listed below. + +--- + +## Responsibilities of Maintainers + +Maintainers are responsible for: + +- Reviewing and responding to **GitHub Issues**. +- Assessing **documentation updates and corrections**. +- Overseeing **code updates** developed by NDTP-approved suppliers. +- Ensuring compliance with **NDTP’s licensing and security policies**. + +NDTP does not accept public code contributions, but we welcome **bug reports and documentation feedback**. + +--- + +## Current Maintainers + +| Name | Organisation | Role | Contact | +|-------------------|------------------------|--------------------|------------------------------| +| Nikan Negaresh | Informed Solutions | Lead Maintainer | nikan.negaresh@informed.com | +| Nikan Negaresh | Informed Solutions | Security Contact | nikan.negaresh@informed.com | +| Nikan Negaresh | Informed Solutions | Documentation Lead | nikan.negaresh@informed.com | + +For general issues, please **open a GitHub issue** rather than contacting maintainers directly. + +--- + +## Escalation Contacts + +If you need to escalate an issue that has not been addressed within a reasonable time: + +1. **Security vulnerabilities** – Follow the responsible disclosure process in [SECURITY.md](./SECURITY.md). +2. **Governance and policy queries** – Contact NDTP at **ndtp@businessandtrade.gov.uk**. +3. **Urgent operational issues** – If an issue affects critical systems, contact the **Lead Maintainer** listed above. + +--- + +## Updating this File + +Maintainer details may change over time. If you are an NDTP-approved maintainer and need to update this file, please submit a request through the designated NDTP repository administrator. + +--- + +**Maintained by the National Digital Twin Programme (NDTP).** + +© 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 NDTP InnerSource Licence – Version 1.0. + +For full licensing terms, see [LICENSE.md](LICENSE.md). \ No newline at end of file diff --git a/README.md b/README.md index f2e8b78..fdb5610 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,20 @@ -# Management Node Module +# README + +**Repository:** `management-node` +**Description:** `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.` +**Repository Status:** `Private – NDTP InnerSource` + +--- ## 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. + +This repository is part of the **National Digital Twin Programme (NDTP)**. It supports the development of secure, modular, and standards-based components for internal use across NDTP projects. + +> **This repository is private and governed by the NDTP InnerSource Licence – Version 1.0.** +> It is intended solely for collaboration among NDTP teams and authorised suppliers. +> It is **not open source** and must not be disclosed, redistributed, or published externally. + +--- ## Prerequisites - Java 21 @@ -460,4 +473,42 @@ 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 +- Setting up monitoring and alerting for security events + +## Public Funding Acknowledgment +This repository has been developed with public funding as part of the National Digital Twin Programme (NDTP), a UK Government initiative. NDTP, alongside its partners, has invested in this work to advance open, secure, and reusable digital twin technologies for any organisation, whether from the public or private sector, irrespective of size. + +## Licensing + +This repository, including all source code, documentation, configuration files, and related materials, is licensed under the: + +**NDTP InnerSource Licence – Version 1.0** +See [LICENSE.md](LICENSE.md) for the full licence text. + +> ⚠️ This repository is **not open source**. +> Redistribution, disclosure, or publication of any part of this repository is prohibited without the **explicit, written approval** of the NDTP Management Team. + +All intellectual property rights are held by the **Department for Business and Trade (UK)** as the governing entity for the National Digital Twin Programme (NDTP). + +## Security and Responsible Disclosure +We take security seriously. If you believe you have found a security vulnerability in this repository, please follow our responsible disclosure process outlined in `SECURITY.md`. + +## Software Bill of Materials (SBOM) + +This project provides a Software Bill of Materials (SBOM) to help users and integrators understand its dependencies. + +### Current SBOM +Download the [latest SBOM for this codebase](../../dependency-graph/sbom) to view the current list of components used in this repository. + +## Contributing +We welcome contributions that align with the Programme’s objectives. Please read our `CONTRIBUTING.md` guidelines before submitting pull requests. + +## Acknowledgements +This repository has benefited from collaboration with various organisations. For a list of acknowledgments, see `ACKNOWLEDGEMENTS.md`. + +## Support and Contact +For questions or support, check our Issues or contact the NDTP team by emailing ndtp@businessandtrade.gov.uk. + +**Maintained by the National Digital Twin Programme (NDTP).** + +© 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. \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..8730a65 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,73 @@ +# Security Policy + +**Repository:** `management-node` +**Description:** `Details the responsible disclosure process for security vulnerabilities.` + +## Responsible Disclosure + +The National Digital Twin Programme (NDTP) follows a **Coordinated Vulnerability Disclosure (CVD)** process to ensure security risks are addressed responsibly. + +By reporting security vulnerabilities through the responsible channels, you agree to: +- Not disclose details of the vulnerability publicly until NDTP has had a reasonable opportunity to fix it. +- Provide NDTP with adequate time to assess and mitigate the risk. +- Act in good faith and follow ethical security research principles. + +NDTP reserves the right to take necessary action against unauthorised or harmful security testing activities. + +--- + +## Reporting Security Issues + +NDTP takes security seriously and encourages responsible reporting of vulnerabilities. + +If you believe you have found a security vulnerability in this repository, **please do not report it publicly**. Instead, follow the steps below to disclose the issue responsibly. + +### **How to Report a Security Issue** + +1. **Do not open a public issue on GitHub.** Instead, report security concerns via email to **ndtp@businessandtrade.gov.uk**. +2. **Provide detailed information about the vulnerability**, including: + - A clear description of the issue. + - Steps to reproduce the vulnerability. + - Potential impact or risk level. + - Any suggested mitigation strategies. +3. **Allow time for assessment and response.** NDTP will review the report and respond within **10 working days** to acknowledge receipt. +4. **Cooperate with NDTP to validate and address the issue.** + +Once a resolution has been identified, NDTP may choose to: + - **Release a patch** as part of the next scheduled update. + - **Issue a security advisory** if the issue is critical. + - **Provide acknowledgments** where appropriate (subject to NDTP’s disclosure policy). + +--- + +## Scope + +This security policy applies to: +- All NDTP repositories released as open source. +- Code, configuration files, and infrastructure deployed as part of NDTP’s **Integration Architecture (IA)**. +- **Third-party dependencies** included within NDTP repositories. If you identify a vulnerability in a third-party component that NDTP relies on (e.g., outdated libraries or known security flaws in dependencies), we encourage you to report it. + +Out of scope: +- Issues related to third-party services or software **not used within NDTP repositories**. +- Vulnerabilities in user environments that are unrelated to this repository. +- Unsolicited security testing or penetration testing without NDTP’s explicit permission. + +--- + +## Security Best Practices + +To help maintain security across NDTP repositories, we follow these principles: +- Dependencies are **scanned and updated regularly** (e.g., using automated tools like Dependabot). +- Sensitive credentials **must not be included** in public repositories. +- Security patches are applied in a timely manner, with priority given to critical vulnerabilities. + +--- + +**Maintained by the National Digital Twin Programme (NDTP).** + +© 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 NDTP InnerSource Licence – Version 1.0. + +For full licensing terms, see [LICENSE.md](LICENSE.md). + From 605c920ba654b986c24af5d52e088402094e25b9 Mon Sep 17 00:00:00 2001 From: Nikan Negaresh <84400913+nikan-negaresh-informed@users.noreply.github.com> Date: Fri, 29 Aug 2025 17:22:34 +0100 Subject: [PATCH 05/13] Feature/dpav 1422 (#6) * - Add development environment configuration with new `application-dev.yml` file - Update `Dockerfile` to include non-root user and improve permission handling - Introduce `Dockerfile-dev` for development-specific container build - Add build and publish shell scripts for Docker workflows - Enhance `JwtToken` to support single-value arrays with `@JsonFormat` annotation - Update SSL properties in `SslPropertyInitializer` to align with new configuration structure - Modify server port and add SSL settings in `application.yml` * Remove `application-dev.yml` configuration file * Adding PR template * feat(OSPO): synchronise OSPO workflows * Add issue templates for bug reports and feature requests * Refine PR template: cleanup formatting and improve readability --- .github/ISSUE_TEMPLATE/bug_report.md | 42 +++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 24 +++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 26 ++++++++++++ .github/workflows/oss-checker.yml | 9 ++-- .github/workflows/publish-github-release.yml | 6 +-- docker/Dockerfile | 26 ++++++++---- docker/Dockerfile-dev | 27 ++++++++++++ docker/build.sh | 4 ++ docker/publish.sh | 14 +++++++ .../config/SslPropertyInitializer.java | 8 ++-- .../node/management/model/jwt/JwtToken.java | 1 + src/main/resources/application.yml | 14 ++++++- 12 files changed, 182 insertions(+), 19 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 docker/Dockerfile-dev create mode 100755 docker/build.sh create mode 100755 docker/publish.sh diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..c0879a3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,42 @@ +**Repository:** `management-node` +**Description:** `Details steps and information required to report issues with the software` +**SPDX-License-Identifier:** OGL-UK-3.0 + +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..2626541 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,24 @@ +**Repository:** `management-node` +**Description:** `Details steps and information required to request new features within the software` +**SPDX-License-Identifier:** OGL-UK-3.0 + +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1dab9b3 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,26 @@ +## Sensitive Credential Checks +- [ ] As the author of these changes, I have checked for any sensitive credentials prior to this review being requested. +- [ ] As a reviewer of these changes, I have checked for any sensitive credentials prior to approving this merge. + + + +## Motivation and Context + + + +## Description + + +## How Has This Been Tested? + + + + +## Screenshots (if appropriate): + +## Checklist: + + +- [ ] It contains only changes required by issue (does not contain other PR) +- [ ] Includes link to an issue (if apply) +- [ ] I have added tests to cover my changes. \ No newline at end of file diff --git a/.github/workflows/oss-checker.yml b/.github/workflows/oss-checker.yml index 2bf4025..65b5f1b 100644 --- a/.github/workflows/oss-checker.yml +++ b/.github/workflows/oss-checker.yml @@ -6,6 +6,9 @@ name: Run OSS check helper on: workflow_dispatch: +permissions: + contents: read + jobs: oss-checks: runs-on: ubuntu-latest @@ -30,19 +33,19 @@ jobs: permission-contents: read - name: Checkout target repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: token: ${{ steps.target_token.outputs.token }} - name: Checkout OSPO source repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 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 + uses: actions/checkout@v5 with: repository: National-Digital-Twin/archetypes path: archetypes diff --git a/.github/workflows/publish-github-release.yml b/.github/workflows/publish-github-release.yml index 23a5669..8e35068 100644 --- a/.github/workflows/publish-github-release.yml +++ b/.github/workflows/publish-github-release.yml @@ -57,7 +57,7 @@ jobs: needs: [versioning] steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Generate SPDX SBOM run: | @@ -83,7 +83,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 @@ -100,7 +100,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download SBOM Artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: sbom diff --git a/docker/Dockerfile b/docker/Dockerfile index 90d0a7e..6a58c5d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -7,21 +7,31 @@ COPY pom.xml . COPY src ./src # Build the application -RUN mvn clean package -DskipTests +RUN mvn -B clean package -DskipTests # Runtime stage FROM eclipse-temurin:21-jdk-alpine +# Create non-root user and group +RUN addgroup -S app && adduser -S -G app -u 10001 app + WORKDIR /app -# Create directory for certificates -RUN mkdir -p /app/docker +# Create directories and set permissions +RUN mkdir -p /app/docker /app/logs /app/tmp && chown -R app:app /app -# Copy application jar from build stage and certificates +# Copy application jar from build stage 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 +RUN chown app:app /app/app.jar + +# Use non-root user from here on +USER app:app + +# Expose HTTPS port +EXPOSE 8443 -EXPOSE 8090 +# Helpful defaults for Java in containers +ENV JAVA_OPTS="-Djava.security.egd=file:/dev/./urandom -XX:MaxRAMPercentage=75.0 -Djava.io.tmpdir=/app/tmp" -ENTRYPOINT ["java", "-jar", "/app/app.jar"] \ No newline at end of file +# Use sh -c so JAVA_OPTS is expanded +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"] \ No newline at end of file diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev new file mode 100644 index 0000000..ff366c7 --- /dev/null +++ b/docker/Dockerfile-dev @@ -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 8443 + +ENTRYPOINT ["java", "-jar", "/app/app.jar"] \ No newline at end of file diff --git a/docker/build.sh b/docker/build.sh new file mode 100755 index 0000000..5b3293b --- /dev/null +++ b/docker/build.sh @@ -0,0 +1,4 @@ +#!/bin/sh -e + +cd .. +sudo docker build -f docker/Dockerfile -t ndtp/management-node . diff --git a/docker/publish.sh b/docker/publish.sh new file mode 100755 index 0000000..f0e278e --- /dev/null +++ b/docker/publish.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +AWS_REGION=eu-west-2 +AWS_ACCOUNT=$(aws sts get-caller-identity --query Account --output text) + +aws ecr get-login-password --region $AWS_REGION | sudo docker login --username AWS --password-stdin $AWS_ACCOUNT.dkr.ecr.$AWS_REGION.amazonaws.com +TAG=1.0.$(date +%s) + +IMAGE=$AWS_ACCOUNT.dkr.ecr.$AWS_REGION.amazonaws.com/ndtp/management-node:$TAG + +sudo docker tag ndtp/management-node $IMAGE +sudo docker push $IMAGE + +echo "Revision tag: $TAG" 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 index 5b6cc38..000971d 100644 --- 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 @@ -7,13 +7,13 @@ @Component public class SslPropertyInitializer { - @Value("${server.ssl.key-store}") + @Value("${application.client.key-store}") private String keyStore; - @Value("${server.ssl.key-store-password}") + @Value("${application.client.key-store-password}") private String keyStorePassword; - @Value("${server.ssl.key-store-type:JKS}") + @Value("${application.client.keyStoreType:JKS}") private String keyStoreType; @Value("${server.ssl.trust-store}") @@ -28,7 +28,7 @@ public class SslPropertyInitializer { @PostConstruct public void init() { System.setProperty("javax.net.ssl.keyStore", keyStore); - System.setProperty("javax.net.ssl.keyStorePassword", keyStorePassword); + System.setProperty("javax.net.ssl.keyStorePassword",keyStorePassword); System.setProperty("javax.net.ssl.keyStoreType", keyStoreType); System.setProperty("javax.net.ssl.trustStore", trustStore); 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 index 157fd32..0752db2 100644 --- 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 @@ -1,5 +1,6 @@ package uk.gov.dbt.ndtp.ia.node.management.model.jwt; +import com.fasterxml.jackson.annotation.JsonFormat; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7ad2d89..8f68c1e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -32,7 +32,7 @@ spring: # Server configuration server: - port: 8090 + port: 8443 ssl: key-alias: localhost key-store: keystore.jks @@ -41,8 +41,20 @@ server: trust-store: truststore.jks trust-store-password: trust-store-type: JKS + client-auth: need + enabled: true +application: + client: + key-store: client-keystore.jks + keyStorePassword: + keyStoreType: JKS + # Actuator Configuration management: + server: + port: 8081 + ssl: + enabled: false endpoints: web: exposure: From a89172d30f3e72ba3e5347d4452398ebab8fc11a Mon Sep 17 00:00:00 2001 From: Nikan Negaresh <84400913+nikan-negaresh-informed@users.noreply.github.com> Date: Fri, 12 Sep 2025 09:25:37 +0100 Subject: [PATCH 06/13] Prepare Repository for public release and enhance configuration (#7) * Add dynamic audience resolution for Federator scopes, update formatting, and rename backend file * Add dynamic audience resolution for Federator scopes, update formatting, and rename backend file * Preparing the repository for public release * Preparing the repository for public release * Update licensing to Open Government Licence v3.0 for public release * Update copyright formatting and align SPDX-License-Identifier across all files * Update copyright formatting and align SPDX-License-Identifier across all files * Update copyright formatting and align SPDX-License-Identifier across all files * Update copyright formatting and align SPDX-License-Identifier across all files * Update copyright formatting and align SPDX-License-Identifier across all files * Update copyright formatting and align SPDX-License-Identifier across all files * Update copyright formatting and align SPDX-License-Identifier across all files --- ACKNOWLEDGEMENTS.md | 32 +- CHANGELOG.md | 36 +- CODE_OF_CONDUCT.md | 106 +- CONTRIBUTING.md | 150 +- LICENSE.md | 299 +- MAINTAINERS.md | 65 +- NOTICE.md | 19 + OGL_LICENSE.md | 17 + README.md | 63 +- SECURITY.md | 103 +- docker/Dockerfile | 9 +- docker/Dockerfile-dev | 11 +- docker/build.sh | 6 + docker/keycloak/README.md | 6 +- docker/keycloak/docker-compose.yml | 5 + docker/keycloak/management-node-realm.json | 3348 ----------------- docker/keycloak/tofu/Make-Cmds.md | 114 - docker/keycloak/tofu/Makefile | 7 +- docker/keycloak/tofu/README.md | 79 +- docker/keycloak/tofu/backend.tf | 5 + .../keycloak/tofu/backends/dev-backend.tfvars | 8 + .../tofu/backends/dev-backend.tfvars | 4 - docker/keycloak/tofu/client_scopes.tf | 31 + docker/keycloak/tofu/clients.tf | 5 +- .../tofu/modules/federator_client/main.tf | 19 +- .../tofu/modules/federator_client/outputs.tf | 4 + .../modules/federator_client/providers.tf | 3 + .../modules/federator_client/variables.tf | 7 +- docker/keycloak/tofu/providers.tf | 3 + docker/keycloak/tofu/realm.tf | 4 + docker/keycloak/tofu/terraform.tfvars | 3 + docker/keycloak/tofu/tfvars/dev.tfvars | 2 + docker/keycloak/tofu/variables.tf | 3 + docker/publish.sh | 6 + pom.xml | 539 +-- .../management/ManagementNodeApplication.java | 13 +- .../management/config/ClientIdMdcFilter.java | 58 +- .../config/CustomJwtAuthenticationToken.java | 26 +- .../KeycloakJwtAuthenticationConverter.java | 74 +- .../management/config/ModelMapperConfig.java | 8 +- .../management/config/SecurityConfig.java | 47 +- .../config/SslPropertyInitializer.java | 10 +- .../v1/ConfigurationController.java | 66 +- .../converter/EntityDtoConverter.java | 16 +- .../converter/impl/ConsumerConverter.java | 14 +- .../impl/OrganisationProducerConverter.java | 35 +- .../converter/impl/ProducerConverter.java | 34 +- .../impl/ProductConsumerConverter.java | 14 +- .../converter/impl/ProductConverter.java | 13 +- .../AuthenticationProcessingException.java | 21 +- .../management/exception/ErrorResponse.java | 14 +- .../exception/JwtClaimParsingException.java | 18 +- .../ResourceAccessParsingException.java | 18 +- .../TokenIntrospectionException.java | 18 +- .../handlers/GlobalExceptionHandler.java | 77 +- .../model/dto/ConsumerConfigDTO.java | 10 +- .../management/model/dto/ConsumerDTO.java | 17 +- .../model/dto/ProducerConfigDTO.java | 9 +- .../management/model/dto/ProducerDTO.java | 21 +- .../model/dto/ProductConsumerDTO.java | 15 +- .../node/management/model/dto/ProductDTO.java | 18 +- .../model/jwt/EnhancedPrincipal.java | 42 +- .../node/management/model/jwt/JwtToken.java | 16 +- .../persistency/entity/Consumer.java | 15 +- .../persistency/entity/Organisation.java | 9 +- .../persistency/entity/Producer.java | 15 +- .../persistency/entity/Product.java | 21 +- .../persistency/entity/ProductConsumer.java | 15 +- .../persistency/entity/ProductConsumerId.java | 20 +- .../ConsumerProviderRepository.java | 11 +- .../repository/ConsumerRepository.java | 12 +- .../repository/OrganisationRepository.java | 9 +- .../repository/ProducerRepository.java | 13 +- .../repository/ProductRepository.java | 12 +- .../service/data/ConsumerService.java | 16 +- .../service/data/OrganisationService.java | 12 +- .../service/data/ProducerService.java | 17 +- .../service/data/ProductConsumerService.java | 14 +- .../service/data/ProductService.java | 14 +- .../data/impl/ConsumerServiceImpl.java | 37 +- .../data/impl/OrganisationServiceImpl.java | 10 +- .../data/impl/ProducerServiceImpl.java | 32 +- .../data/impl/ProductConsumerServiceImpl.java | 17 +- .../service/data/impl/ProductServiceImpl.java | 27 +- .../configuration/ConfigurationProvider.java | 20 +- .../ConfigurationProviderImpl.java | 400 +- src/main/resources/application.yml | 22 +- ...20250728142253__intial_database_tables.sql | 18 +- .../samples/V20250728152300__sample_data.sql | 51 +- .../ManagementNodeApplicationTests.java | 12 +- ...tAuthenticationConverterExceptionTest.java | 125 +- ...eycloakJwtAuthenticationConverterTest.java | 286 +- .../v1/ConfigurationControllerTest.java | 51 +- .../converter/impl/ConsumerConverterTest.java | 37 +- .../OrganisationProducerConverterTest.java | 121 +- .../converter/impl/ProducerConverterTest.java | 121 +- .../impl/ProductConsumerConverterTest.java | 19 +- .../converter/impl/ProductConverterTest.java | 37 +- ...AuthenticationProcessingExceptionTest.java | 17 +- .../exception/SpecificExceptionsTest.java | 33 +- .../handlers/GlobalExceptionHandlerTest.java | 18 +- ...erProviderOrganisationServiceImplTest.java | 35 +- .../data/impl/ConsumerServiceImplTest.java | 23 +- .../impl/OrganisationServiceImplTest.java | 12 +- .../data/impl/ProducerServiceImplTest.java | 19 +- .../impl/ProductConsumerServiceImplTest.java | 25 +- .../data/impl/ProductServiceImplTest.java | 19 +- .../ConfigurationProviderImplTest.java | 168 +- 108 files changed, 2538 insertions(+), 5362 deletions(-) create mode 100644 NOTICE.md create mode 100644 OGL_LICENSE.md delete mode 100644 docker/keycloak/management-node-realm.json delete mode 100644 docker/keycloak/tofu/Make-Cmds.md create mode 100644 docker/keycloak/tofu/backends/dev-backend.tfvars delete mode 100644 docker/keycloak/tofu/backends/dev-backend.tfvars diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index 69eef92..a510a81 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -1,29 +1,29 @@ # Acknowledgements **Repository:** `management-node` -**Description:** `Recognises suppliers, partner organisations, and other contributors to the repository's development.` - -The National Digital Twin Programme (NDTP) would like to acknowledge the contributions of various organisations and individuals who have supported the development of this repository. +**Description:** `Recognises suppliers, partner organisations, and other contributors to the repository’s development.` +**SPDX-License-Identifier:** `OGL-UK-3.0` +--- +The National Digital Twin Programme (NDTP) would like to acknowledge the contributions of various organisations and individuals +who have supported the development of this repository. ## Organisational contributions - -Over time, the following organisations have provided technical expertise, development support, and domain knowledge that have contributed to the evolution of this project: +Over time, the following organisations have provided technical expertise, development support, and domain knowledge +that have contributed to the evolution of this project: - [Informed Solutions](https://informed.com) We are grateful for the collaboration that has helped shape this repository. +## Individual contributions -## Individual contributions - -For a list of individual contributors who have made direct commits to this repository, see GitHub’s auto-generated contributor insights: [Contributors](../../../graphs/contributors). - ---- - -**Note:** This acknowledgment does not confer any legal rights, ownership, or imply ongoing involvement by any of the named organisations or individuals. All contributions are made in accordance with the repository’s licensing terms. - -© 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. +For a list of individual contributors who have made direct commits to this repository, see +GitHub’s auto-generated contributor insights: [Contributors](https://github.com/National-Digital-Twin/your-repo/graphs/contributors). -Licensed under the NDTP InnerSource Licence – Version 1.0. +--- -For full licensing terms, see [LICENSE.md](LICENSE.md). +**Note:** This acknowledgment does not confer any legal rights, ownership, or imply ongoing involvement by any of the named organisations or individuals. +All contributions are made in accordance with the repository’s licensing terms. +© 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. +For full licensing terms, see [OGL_LICENSE.md](OGL_LICENSE.md). diff --git a/CHANGELOG.md b/CHANGELOG.md index 54e5df1..6b87157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ **Repository:** `management-node` **Description:** `Tracks all notable changes, version history, and roadmap toward 1.0.0 following Semantic Versioning.` +**SPDX-License-Identifier:** OGL-UK-3.0 + All notable changes to this repository will be documented in this file. @@ -30,6 +32,23 @@ This project follows **Semantic Versioning (SemVer)** ([semver.org](https://semv --- +## [0.90.0] - 2025-09-09 + +### Initial release +- This is the first initial changelog entry for the management-node. It introduces the baseline feature set and establishes the changelog structure following Semantic Versioning. + +### Added +- Core domain and persistence for producers and consumers (JPA entities, repositories, and services). +- REST APIs for managing producers/consumers and related configurations (v1 controllers and DTOs). +- Configuration management provider for node settings and environment-driven overrides. +- Security integration with Keycloak (realm configuration and OAuth2/OIDC resource server setup). +- TLS/Mutual‑TLS support and related documentation (see docs/MTLS_CONFIGURATION.md). +- Health, readiness, and metrics endpoints (Spring Boot Actuator defaults where applicable). +- Test coverage setup and guidance (Mockito usage and JaCoCo reporting docs). +- Docker and local development assets (compose files, Keycloak realm, publish script, local certs/truststore). + +--- + ## Future Roadmap to `1.0.0` The `0.90.x` series is part of NDTP’s **pre-stable development cycle**, meaning: @@ -41,14 +60,13 @@ Once `1.0.0` is reached, future versions will follow **strict SemVer rules**. --- -## Versioning Policy - -1. **MAJOR updates (`X.0.0`)** – Typically introduce breaking changes that require users to modify their code or configurations. -- **Breaking changes (default rule)**: Any backward-incompatible modifications require a major version bump. -- **Non-breaking major updates (exceptional cases)**: A major version may also be incremented if the update represents a significant milestone, such as a shift in governance, a long-term stability commitment, or substantial new functionality that redefines the project’s scope. -2. **MINOR updates (`0.X.0`)** – New functionality that is backward-compatible. -3. **PATCH updates (`0.0.X`)** – Bug fixes, performance improvements, or security patches. -4. **Dependency updates** – A **major dependency upgrade** that introduces breaking changes should trigger a **MAJOR** version bump (once at `1.0.0`). +## Versioning Policy +1. **MAJOR updates (`X.0.0`)** – Typically introduce breaking changes that require users to modify their code or configurations. + - **Breaking changes (default rule)**: Any backward-incompatible modifications require a major version bump. + - **Non-breaking major updates (exceptional cases)**: A major version may also be incremented if the update represents a significant milestone, such as a shift in governance, a long-term stability commitment, or substantial new functionality that redefines the project’s scope. +2. **MINOR updates (`0.X.0`)** – New functionality that is backward-compatible. +3. **PATCH updates (`0.0.X`)** – Bug fixes, performance improvements, or security patches. +4. **Dependency updates** – A **major dependency upgrade** that introduces breaking changes should trigger a **MAJOR** version bump (once at `1.0.0`). --- @@ -65,7 +83,7 @@ Once `1.0.0` is reached, future versions will follow **strict SemVer rules**. © 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 NDTP InnerSource Licence – Version 1.0. +Licensed under the Open Government Licence v3.0. For full licensing terms, see [LICENSE.md](LICENSE.md). diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index ec0ded5..0dccab8 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,80 +2,54 @@ **Repository:** `management-node` **Description:** `Defines expected behaviors, rules, and the enforcement process to ensure professional engagement.` +**SPDX-License-Identifier:** OGL-UK-3.0 -## Introduction +## Introduction +The National Digital Twin Programme (NDTP) is committed to fostering an open, inclusive, and professional environment in all its public repositories. +This Code of Conduct outlines the expectations for behaviour when engaging with NDTP repositories, including issue reporting, documentation feedback, +and discussions with repository maintainers. -The National Digital Twin Programme (NDTP) is committed to fostering an open, inclusive, and professional environment in all its repositories. -This Code of Conduct outlines the expectations for behaviour when engaging with NDTP repositories, including issue reporting, documentation feedback, -and discussions with repository maintainers. - -By participating in this repository, you agree to follow this Code of Conduct. +By participating in this repository, you agree to follow this Code of Conduct. --- - -## Expected Behaviour - -All contributors, maintainers, and public users are expected to: - -- **Be respectful and professional** – Treat others with courtesy and professionalism. -- **Communicate constructively** – Offer feedback that is clear, helpful, and focused on improving the repository. -- **Engage in a welcoming manner** – Encourage participation and provide a positive experience for all users. -- **Provide relevant and clear information** – When submitting issues or feedback, be specific and include details that help maintainers understand the request. - +## Expected Behaviour +All contributors, maintainers, and public users are expected to: +- **Be respectful and professional** – Treat others with courtesy and professionalism. +- **Communicate constructively** – Offer feedback that is clear, helpful, and focused on improving the repository. +- **Engage in a welcoming manner** – Encourage participation and provide a positive experience for all users. +- **Provide relevant and clear information** – When submitting issues or feedback, be specific and include details that help maintainers understand the request. --- - -## Unacceptable Behaviour - -The following behaviour will not be tolerated: - -- **Harassment, discrimination, or personal attacks** – Any form of offensive behaviour towards individuals or groups. -- **Trolling, disruptive comments, or inflammatory language** – Intentionally provoking arguments or making non-constructive comments. -- **Excessive demands or unrealistic expectations of maintainers** – This includes repeated requests for prioritisation outside of programme priorities. -- **Spamming or promotional content** – Off-topic discussions unrelated to the repository's purpose. -- **Disclosing sensitive information** – Sharing security vulnerabilities or confidential details outside of the responsible disclosure process. - +## Unacceptable Behaviour +The following behaviour will not be tolerated: +- **Harassment, discrimination, or personal attacks** – Any form of offensive behaviour towards individuals or groups. +- **Trolling, disruptive comments, or inflammatory language** – Intentionally provoking arguments or making non-constructive comments. +- **Excessive demands or unrealistic expectations of maintainers** – This includes repeated requests for prioritisation outside of programme priorities. +- **Spamming or promotional content** – Off-topic discussions unrelated to the repository's purpose. +- **Disclosing sensitive information** – Sharing security vulnerabilities or confidential details outside of the responsible disclosure process. --- - -## Reporting Concerns - -If you believe someone is violating this Code of Conduct, please report it by following these steps: - -1. **For general issues** – Raise a concern with the repository maintainers by emailing ndtp@businessandtrade.gov.uk. -2. **For security-related concerns** – Follow the responsible disclosure process outlined in [SECURITY.md](./SECURITY.md). -3. **For incidents requiring escalation** – NDTP reserves the right to take appropriate action, including restricting access to contributors who violate this policy. - -All reports will be reviewed confidentially, and NDTP will take appropriate action to address the issue. - +## Reporting Concerns +If you believe someone is violating this Code of Conduct, please report it by following these steps: +1. **For general issues** – Raise a concern with the repository maintainers by emailing ndtp@businessandtrade.gov.uk. +2. **For security-related concerns** – Follow the responsible disclosure process outlined in [SECURITY.md](SECURITY.md). +3. **For incidents requiring escalation** – NDTP reserves the right to take appropriate action, including restricting access to contributors who violate this policy. + All reports will be reviewed confidentially, and NDTP will take appropriate action to address the issue. --- - -## Enforcement - +## Enforcement Violations of this Code of Conduct may result in: - -- A formal warning -- Temporary suspension from participation -- Permanent exclusion from engaging with NDTP repositories - -Decisions on enforcement are made at NDTP’s discretion. - +- A formal warning +- Temporary suspension from participation +- Permanent exclusion from engaging with NDTP repositories + Decisions on enforcement are made at NDTP’s discretion. --- - -## Scope - -This Code of Conduct applies to all interactions in NDTP repositories, including but not limited to: - -- Issue tracking and reporting -- Documentation suggestions and feedback -- Discussions with maintainers -- Any other interactions in public NDTP projects - +## Scope +This Code of Conduct applies to all interactions in NDTP repositories, including but not limited to: +- Issue tracking and reporting +- Documentation suggestions and feedback +- Discussions with maintainers +- Any other interactions in public NDTP projects --- - -**Maintained by the National Digital Twin Programme (NDTP).** - -© 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 NDTP InnerSource Licence – Version 1.0. - -For full licensing terms, see [LICENSE.md](LICENSE.md). +**Maintained by the National Digital Twin Programme (NDTP).** +© 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. +For full licensing terms, see [OGL_LICENSE.md](OGL_LICENSE.md). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff7b98d..f8be9d4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,86 +2,64 @@ **Repository:** `management-node` **Description:** `Guidelines for issue reporting, documentation suggestions, and NDTP’s controlled contribution model.` - -Thank you for your interest in this repository. - -The National Digital Twin Programme (NDTP) develops and maintains this repository in collaboration with suppliers and partner organisations, including other parts of government and their suppliers. - -NDTP follows a **Cathedral open-source governance model** where code may be made **publicly available** under open-source licences, and collaboration is invited from **approved partners**. Contributions from the general public are not currently accepted, but **feedback, issue reporting, and documentation suggestions are encouraged**. - -If you want to see which suppliers and organisations have contributed to this repository in the past, refer to [ACKNOWLEDGEMENTS.md](./ACKNOWLEDGEMENTS.md) and the GitHub contributor insights page at [Contributors](../../graphs/contributors). +**SPDX-License-Identifier:** `OGL-UK-3.0` --- -## How You Can Contribute +Thank you for your interest in this repository. +The National Digital Twin Programme (NDTP) develops and maintains this repository in collaboration with suppliers and partner organisations, including other parts of +government and their suppliers. +NDTP follows an **open-source governance model** where all code is **publicly available** under open-source licences, and collaboration is invited from **approved +partners**. Contributions from the general public are not currently accepted, but **feedback, issue reporting, and documentation suggestions are encouraged**. +If you want to see which suppliers and organisations have contributed to this repository in the past, refer to [ACKNOWLEDGEMENTS.md](ACKNOWLEDGEMENTS.md) and the GitHub +contributor insights page at [Contributors](https://github.com/National-Digital-Twin/your-repo/graphs/contributors). -Public users and NDTP partners are encouraged to engage in the following ways: +--- -- **Reporting bugs and issues** – If you find a problem, please open a GitHub issue. -- **Suggesting documentation improvements** – Propose clarifications or additions to the existing documentation. +## How You Can Contribute +Public users and NDTP partners are encouraged to engage in the following ways: +- **Reporting bugs and issues** – If you find a problem, please open a GitHub issue. +- **Suggesting documentation improvements** – Propose clarifications or additions to the existing documentation. - **Providing structured feedback** – If you have suggestions for improvements, let us know via GitHub Issues. - -While we review all input, NDTP prioritises development based on programme goals, supplier development cycles, and strategic objectives. - -NDTP does not currently accept **public pull requests (PRs) or direct code contributions** to this repository. Contributions are limited to **approved suppliers and partner organisations** under formal agreements. - -For details on repository maintainers and how to contact them, refer to [MAINTAINERS.md](./MAINTAINERS.md). - + While we review all input, NDTP prioritises development based on programme goals, supplier development cycles, and strategic objectives. + NDTP does not currently accept **public pull requests (PRs) or direct code contributions** to this repository. Contributions are limited to **approved suppliers and + partner organisations** under formal agreements. + For details on repository maintainers and how to contact them, refer to [MAINTAINERS.md](MAINTAINERS.md). --- - -## Reporting Issues - -If you encounter a bug, error, or inconsistency, please follow these steps: - -1. Check for an existing issue under [Issues](../../issues). -2. Open a new issue if no one has reported it yet. Use one of the provided issue templates. -3. Provide a clear, detailed description of the issue, including steps to reproduce it if applicable. +## Reporting Issues +If you encounter a bug, error, or inconsistency, please follow these steps: +1. Check for an existing issue under [Issues](https://github.com/National-Digital-Twin/your-repo/issues). +2. Open a new issue if no one has reported it yet. Use one of the provided issue templates. +3. Provide a clear, detailed description of the issue, including steps to reproduce it if applicable. 4. Label the issue appropriately (bug, documentation, enhancement, etc.). - -For security-related issues, do not submit a public issue. Instead, follow our [Responsible Disclosure process](./SECURITY.md). - + For security-related issues, do not submit a public issue. Instead, follow our [Responsible Disclosure process](SECURITY.md). --- - -## Documentation Feedback - -If you find an error in the documentation, need more clarity, or have suggestions for additional documentation, you can: - -1. Open a GitHub issue under the `documentation` label. -2. Describe the improvement you are suggesting, including references to existing documentation where applicable. +## Documentation Feedback +If you find an error in the documentation, need more clarity, or have suggestions for additional documentation, you can: +1. Open a GitHub issue under the `documentation` label. +2. Describe the improvement you are suggesting, including references to existing documentation where applicable. 3. Submit structured feedback – specific examples help us make updates faster. - -We prioritise documentation updates based on user impact and alignment with programme goals. - + We prioritise documentation updates based on user impact and alignment with programme goals. --- - -## NDTP's Approach to Open-Source Development - -- **Development is led by approved suppliers and partners** who have been engaged through a formal process. +## NDTP's Approach to Open-Source Development +- **All NDTP code is publicly available under open-source licences.** +- **Development is led by approved suppliers and partners** who have been engaged through a formal process. - **We welcome feedback and ideas**, but implementation is subject to programme priorities. - -To see what we’re working on, check out our [Project Roadmap](../../projects). If no roadmap is currently available, please note that it is being actively developed and will be published in due course. - + To see what we’re working on, check out our [Project Roadmap](https://github.com/National-Digital-Twin/management-node/projects). If no roadmap is currently available, + please note that it is being actively developed and will be published in due course. --- - -## Branching Strategy - -This repository follows a **GitFlow-based branching model** to manage development efficiently. Key conventions include: - -- **Main Branch (`main`)**: The stable, production-ready branch. Only tested and approved changes are merged here. -- **Develop Branch (`develop`)**: The integration branch where features and fixes are merged before reaching `main`. -- **Feature Branches (`feature/*`)**: Used for new developments. Named based on functionality, e.g., `feature/new-auth-method`. -- **Bugfix Branches (`bugfix/*`)**: Address minor issues in `develop` before release. -- **Release Branches (`release/*`)**: Used to prepare a new stable release, ensuring final testing and versioning updates. +## Branching Strategy +This repository follows a **GitFlow-based branching model** to manage development efficiently. Key conventions include: +- **Main Branch (`main`)**: The stable, production-ready branch. Only tested and approved changes are merged here. +- **Develop Branch (`develop`)**: The integration branch where features and fixes are merged before reaching `main`. +- **Feature Branches (`feature/*`)**: Used for new developments. Named based on functionality, e.g., `feature/new-auth-method`. +- **Bugfix Branches (`bugfix/*`)**: Address minor issues in `develop` before release. +- **Release Branches (`release/*`)**: Used to prepare a new stable release, ensuring final testing and versioning updates. - **Hotfix Branches (`hotfix/*`)**: Critical fixes applied directly to `main` and merged back into `develop`. - -For more details, refer to [GitFlow Workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow). - + For more details, refer to [GitFlow Workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow). --- - -## Pull Request Policy - -To maintain high-quality contributions, NDTP enforces the following **minimum pull request (PR) requirements** for approved contributors: - +## Pull Request Policy +To maintain high-quality contributions, NDTP enforces the following **minimum pull request (PR) requirements** for approved contributors: - **All PRs must be reviewed by at least one maintainer** before merging. - **PRs should reference a corresponding issue** where applicable. - **Code changes must include relevant tests** to ensure stability. @@ -90,34 +68,24 @@ To maintain high-quality contributions, NDTP enforces the following **minimum pu - **PRs should use "squash and merge" as the preferred merge strategy**, ensuring a clean history. - **Feature and bugfix branches should be deleted after merge** to keep the repository tidy. - **Force pushing to the `main` branch is strictly prohibited** to protect repository integrity. -- **CI builds must pass before merging** to enforce basic validation checks. - +- **CI builds must pass before merging** to enforce basic validation checks. + For further details, see [CONTRIBUTING.md](CONTRIBUTING.md). --- - -## Contribution Licensing - -By submitting feedback, documentation suggestions, or issue reports, you acknowledge that any resulting changes will be licensed under the same open-source terms as this repository: - -- Code contributions (if ever accepted) will be licensed under Apache 2.0. +## Contribution Licensing +By submitting feedback, documentation suggestions, or issue reports, you acknowledge that any resulting changes will be licensed under the same open-source terms +as this repository: +- Code contributions (if ever accepted) will be licensed under Apache 2.0. - Documentation updates will be licensed under OGL v3.0. - -For supplier-contracted development, NDTP ensures that all contributions align with Crown Copyright and public sector open-source standards. - + For supplier-contracted development, NDTP ensures that all contributions align with Crown Copyright and public sector open-source standards. --- +## Repository Maintainers +For details on who maintains this repository and how to contact them, refer to [MAINTAINERS.md](MAINTAINERS.md). -## Repository Maintainers - -For details on who maintains this repository and how to contact them, refer to [MAINTAINERS.md](./MAINTAINERS.md). - -NDTP repository maintainers review reported issues, evaluate documentation suggestions, and oversee ongoing development. - ---- - -**Maintained by the National Digital Twin Programme (NDTP).** - -© 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 NDTP InnerSource Licence – Version 1.0. - -For full licensing terms, see [LICENSE.md](LICENSE.md). +NDTP repository maintainers review reported issues, evaluate documentation suggestions, and oversee ongoing development. +--- +Maintained by the National Digital Twin Programme (NDTP). +© 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. +For full licensing terms, see [OGL_LICENSE.md](OGL_LICENSE.md). \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md index c334957..e8f3ef1 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,96 +1,223 @@ -# NDTP InnerSource License +# License **Repository:** `management-node` **Description:** `Defines the licensing terms for the source code in this repository.` +**SPDX-License-Identifier:** `Apache-2.0` ---- - -## Version - -**NDTP InnerSource License – Version 1.0** -**Issued by:** National Digital Twin Programme (NDTP) -**Effective Date:** 9 July 2025 - ---- - -## Copyright - -© Crown Copyright 2025. -This work has been developed by the **National Digital Twin Programme (NDTP)** and is legally attributed to the **Department for Business and Trade (UK)** as the governing entity. - -This repository is **not open source**. -Its contents are licensed under the terms of this **NDTP InnerSource License**, unless and until it is formally published under an approved open source licence by the NDTP Management Team. - ---- - -## 1. Purpose - -This repository supports InnerSource development practices within the NDTP. It enables collaborative development by internal teams and authorised suppliers, in a controlled and non-public environment. - ---- - -## 2. Licensing Status - -This work is **not licensed under an open source licence**. -It must not be published, distributed, sublicensed, or shared externally without the **explicit, written approval** of the NDTP Management Team. - -> The NDTP InnerSource Licence permits internal collaboration only. No part of this repository may be used or disclosed beyond the authorised delivery context. - ---- - -## 3. Intellectual Property - -All rights, including intellectual property rights in this code and associated materials, are owned by the NDTP. - -Where contributions are made by suppliers or delivery partners, those contributions are accepted on the basis that **full intellectual property rights** belong to the Crown under the terms of their contract. - ---- - -## 4. Permitted Use - -You may: - -- View, use, and modify the code as required to fulfil your responsibilities under the NDTP. -- Collaborate within authorised NDTP teams and with approved suppliers under existing contracts. - -You may not: - -- Share, publish, or release this repository publicly. -- Fork, clone, or redistribute this code outside approved NDTP channels. -- Apply any license other than the NDTP InnerSource License to this repository or its contents, unless instructed by the NDTP Management Team. - ---- - -## 5. Future Publication - -At the discretion of the NDTP Management Team, this repository may later be designated for release under an approved open source licence. - -Any such designation must follow NDTP's internal governance processes. - -> Until such designation is explicitly made and executed, this repository remains **confidential and proprietary**. - ---- - -## 6. Enforcement - -Any unauthorised disclosure, publication, or redistribution of this repository or its contents may: - -- Constitute a breach of contract -- Trigger formal investigation -- Result in legal, disciplinary, or commercial action, including revocation of access rights - -All actions will be escalated to the NDTP Management Team for appropriate handling. - ---- -## Contact +## Copyright Notice -For all enquiries regarding licensing, publication status, or contributor rights, please contact: +© 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. -**NDTP Management Team** -Department for Business and Trade (UK) -NDTP@BUSINESSANDTRADE.GOV.UK +This work is licensed under the Apache License, Version 2.0. +**Note:** All documentation in this repository is licensed under the Open Government Licence v3.0 (OGL-3.0). See [OGL_LICENSE.md](OGL_LICENSE.md) for full terms. --- -**End of NDTP InnerSource Licence – Version 1.0** +# Apache License + +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2025 Crown Copyright, National Digital Twin Programme, +legally attributed to the Department for Business and Trade (UK) + +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. \ No newline at end of file diff --git a/MAINTAINERS.md b/MAINTAINERS.md index a3a1ba6..a3f7d46 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -1,64 +1,69 @@ -# Maintainers +# Maintainers **Repository:** `management-node` **Description:** `Lists maintainers responsible for reviewing issues, security, and documentation updates.` +**SPDX-License-Identifier:** OGL-UK-3.0 -## Introduction +## Introduction -This repository is maintained by the **National Digital Twin Programme (NDTP)** in collaboration with contracted suppliers and partner organisations. +This repository is maintained by the **National Digital Twin Programme (NDTP)** in collaboration with contracted +suppliers and partner organisations. -Maintainers are responsible for reviewing issues, evaluating documentation suggestions, and overseeing -supplier-led development. +Maintainers are responsible for reviewing issues, evaluating documentation suggestions, and overseeing +supplier-led development. -If you need to report a problem, suggest improvements, or seek guidance on using this repository, please refer to the contacts listed below. +If you need to report a problem, suggest improvements, or seek guidance on using this repository, please refer to the +contacts listed below. --- -## Responsibilities of Maintainers +## Responsibilities of Maintainers -Maintainers are responsible for: +Maintainers are responsible for: -- Reviewing and responding to **GitHub Issues**. -- Assessing **documentation updates and corrections**. -- Overseeing **code updates** developed by NDTP-approved suppliers. -- Ensuring compliance with **NDTP’s licensing and security policies**. +- Reviewing and responding to **GitHub Issues**. +- Assessing **documentation updates and corrections**. +- Overseeing **code updates** developed by NDTP-approved suppliers. +- Ensuring compliance with **NDTP’s licensing and security policies**. -NDTP does not accept public code contributions, but we welcome **bug reports and documentation feedback**. +NDTP does not accept public code contributions, but we welcome **bug reports and documentation feedback**. --- -## Current Maintainers +## Current Maintainers -| Name | Organisation | Role | Contact | -|-------------------|------------------------|--------------------|------------------------------| -| Nikan Negaresh | Informed Solutions | Lead Maintainer | nikan.negaresh@informed.com | -| Nikan Negaresh | Informed Solutions | Security Contact | nikan.negaresh@informed.com | -| Nikan Negaresh | Informed Solutions | Documentation Lead | nikan.negaresh@informed.com | +| Name | Organisation | Role | Contact | +|----------------|--------------------|--------------------|-----------------------| +| Nikan Negaresh | Informed Solutions | Lead Maintainer | NDTP-OSS@informed.com | +| Nikan Negaresh | Informed Solutions | Security Contact | NDTP-OSS@informed.com | +| Nikan Negaresh | Informed Solutions | Documentation Lead | NDTP-OSS@informed.com | -For general issues, please **open a GitHub issue** rather than contacting maintainers directly. +For general issues, please **open a GitHub issue** rather than contacting maintainers directly. --- -## Escalation Contacts +## Escalation Contacts -If you need to escalate an issue that has not been addressed within a reasonable time: +If you need to escalate an issue that has not been addressed within a reasonable time: -1. **Security vulnerabilities** – Follow the responsible disclosure process in [SECURITY.md](./SECURITY.md). -2. **Governance and policy queries** – Contact NDTP at **ndtp@businessandtrade.gov.uk**. -3. **Urgent operational issues** – If an issue affects critical systems, contact the **Lead Maintainer** listed above. +1. **Security vulnerabilities** – Follow the responsible disclosure process in [SECURITY.md](./SECURITY.md). +2. **Governance and policy queries** – Contact NDTP at **ndtp@businessandtrade.gov.uk**. +3. **Urgent operational issues** – If an issue affects critical systems, contact the **Lead Maintainer** listed above. --- -## Updating this File +## Updating this File -Maintainer details may change over time. If you are an NDTP-approved maintainer and need to update this file, please submit a request through the designated NDTP repository administrator. +Maintainer details may change over time. If you are an NDTP-approved maintainer and need to update this file, please +submit a request through the designated NDTP repository administrator. --- -**Maintained by the National Digital Twin Programme (NDTP).** +**Maintained by the National Digital Twin Programme (NDTP).** -© 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. +© 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 NDTP InnerSource Licence – Version 1.0. +Licensed under the Open Government Licence v3.0. For full licensing terms, see [LICENSE.md](LICENSE.md). \ No newline at end of file diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 0000000..6799602 --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,19 @@ +# NOTICE + +**Repository:** `management-node` +**Description:** `Attribution and legal notices related to the use of this repository, including acknowledgments of external contributions.` +**SPDX-License-Identifier:** `OGL-UK-3.0` + +--- +This repository contains software developed as part of the +National Digital Twin Programme (NDTP), a UK Government initiative. +© 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. +## License +This repository contains **both source code and documentation**, each covered by different licenses: +- **Code**: Licensed under the **[Apache License 2.0](LICENSE.md)**. +- **Documentation**: Licensed under the **[Open Government Licence v3.0 (OGL-UK-3.0)](OGL_LICENSE.md)**. + See `LICENSE.md` and `OGL_LICENCE.md`for details. + This project has been developed to support NDTP’s mission of enabling + secure, scalable, and interoperable data-sharing across organisations. + For a list of acknowledgments, see [ACKNOWLEDGEMENTS.md](ACKNOWLEDGEMENTS.md). \ No newline at end of file diff --git a/OGL_LICENSE.md b/OGL_LICENSE.md new file mode 100644 index 0000000..a56cdaf --- /dev/null +++ b/OGL_LICENSE.md @@ -0,0 +1,17 @@ +# Open Government Licence v3.0 + +**Repository:** `management-node` +**Description:** `Covers all documentation files in this repository that are released under the Open Government Licence v3.0.` +**SPDX-License-Identifier:** `OGL-UK-3.0` + + +This repository contains documentation licensed under the Open Government Licence (OGL) v3.0. +You are encouraged to use and re-use the information that is available under this licence. + +## Copyright Notice + +© 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. + +You can view the full license at: +https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/ diff --git a/README.md b/README.md index fdb5610..ffb713c 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,14 @@ # README **Repository:** `management-node` -**Description:** `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.` -**Repository Status:** `Private – NDTP InnerSource` +**Description:** `Provides APIs to be accessed by Consumer and Producer Federators for the purpose of dynamic configuration management ` +**SPDX-License-Identifier:** `Apache-2.0 AND OGL-UK-3.0 ` --- ## Overview -This repository is part of the **National Digital Twin Programme (NDTP)**. It supports the development of secure, modular, and standards-based components for internal use across NDTP projects. - -> **This repository is private and governed by the NDTP InnerSource Licence – Version 1.0.** -> It is intended solely for collaboration among NDTP teams and authorised suppliers. -> It is **not open source** and must not be disclosed, redistributed, or published externally. +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. --- @@ -475,40 +471,25 @@ For production deployments, consider: - Regularly rotating secrets and certificates - Setting up monitoring and alerting for security events -## Public Funding Acknowledgment -This repository has been developed with public funding as part of the National Digital Twin Programme (NDTP), a UK Government initiative. NDTP, alongside its partners, has invested in this work to advance open, secure, and reusable digital twin technologies for any organisation, whether from the public or private sector, irrespective of size. - -## Licensing - -This repository, including all source code, documentation, configuration files, and related materials, is licensed under the: - -**NDTP InnerSource Licence – Version 1.0** -See [LICENSE.md](LICENSE.md) for the full licence text. - -> ⚠️ This repository is **not open source**. -> Redistribution, disclosure, or publication of any part of this repository is prohibited without the **explicit, written approval** of the NDTP Management Team. - -All intellectual property rights are held by the **Department for Business and Trade (UK)** as the governing entity for the National Digital Twin Programme (NDTP). - -## Security and Responsible Disclosure -We take security seriously. If you believe you have found a security vulnerability in this repository, please follow our responsible disclosure process outlined in `SECURITY.md`. - +## Public Funding Acknowledgment +This repository has been developed with public funding as part of the National Digital Twin Programme (NDTP), a UK Government initiative. NDTP, alongside its partners, has invested in this work to advance open, secure, and reusable digital twin technologies for any organisation, whether from the public or private sector, irrespective of size. +## License +This repository contains both source code and documentation, which are covered by different licenses: +- **Code:** Developed and maintained by National Digital Twin Programme. Licensed under the Apache License 2.0. +- **Documentation:** Licensed under the Open Government Licence v3.0. + See `LICENSE.md`, `OGL_LICENCE.md`, and `NOTICE.md` for details. +## Security and Responsible Disclosure +We take security seriously. If you believe you have found a security vulnerability in this repository, please follow our responsible disclosure process outlined in `SECURITY.md`. ## Software Bill of Materials (SBOM) - This project provides a Software Bill of Materials (SBOM) to help users and integrators understand its dependencies. - ### Current SBOM -Download the [latest SBOM for this codebase](../../dependency-graph/sbom) to view the current list of components used in this repository. - -## Contributing -We welcome contributions that align with the Programme’s objectives. Please read our `CONTRIBUTING.md` guidelines before submitting pull requests. - -## Acknowledgements -This repository has benefited from collaboration with various organisations. For a list of acknowledgments, see `ACKNOWLEDGEMENTS.md`. - -## Support and Contact -For questions or support, check our Issues or contact the NDTP team by emailing ndtp@businessandtrade.gov.uk. - -**Maintained by the National Digital Twin Programme (NDTP).** - -© 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. \ No newline at end of file +Download the [latest SBOM for this codebase](https://github.com/[repository-name]/dependency-graph/sbom) to view the current list of components used in this repository. +## Contributing +We welcome contributions that align with the Programme’s objectives. Please read our `CONTRIBUTING.md` guidelines before submitting pull requests. +## Acknowledgements +This repository has benefited from collaboration with various organisations. For a list of acknowledgments, see `ACKNOWLEDGEMENTS.md`. +## Support and Contact +For questions or support, check our Issues or contact the NDTP team on ndtp@businessandtrade.gov.uk. + +**Maintained by the National Digital Twin Programme (NDTP).** +© 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 entityright 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. \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md index 8730a65..22fb8d2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,72 +2,65 @@ **Repository:** `management-node` **Description:** `Details the responsible disclosure process for security vulnerabilities.` +**SPDX-License-Identifier:** `OGL-UK-3.0` +## Responsible Disclosure +The National Digital Twin Programme (NDTP) follows a **Coordinated Vulnerability Disclosure (CVD)** process to ensure security risks are addressed responsibly. -## Responsible Disclosure +By reporting security vulnerabilities through the responsible channels, you agree to: +- Not disclose details of the vulnerability publicly until NDTP has had a reasonable opportunity to fix it. +- Provide NDTP with adequate time to assess and mitigate the risk. +- Act in good faith and follow ethical security research principles. -The National Digital Twin Programme (NDTP) follows a **Coordinated Vulnerability Disclosure (CVD)** process to ensure security risks are addressed responsibly. - -By reporting security vulnerabilities through the responsible channels, you agree to: -- Not disclose details of the vulnerability publicly until NDTP has had a reasonable opportunity to fix it. -- Provide NDTP with adequate time to assess and mitigate the risk. -- Act in good faith and follow ethical security research principles. - -NDTP reserves the right to take necessary action against unauthorised or harmful security testing activities. +NDTP reserves the right to take necessary action against unauthorised or harmful security testing activities. --- -## Reporting Security Issues - -NDTP takes security seriously and encourages responsible reporting of vulnerabilities. - -If you believe you have found a security vulnerability in this repository, **please do not report it publicly**. Instead, follow the steps below to disclose the issue responsibly. - -### **How to Report a Security Issue** - -1. **Do not open a public issue on GitHub.** Instead, report security concerns via email to **ndtp@businessandtrade.gov.uk**. -2. **Provide detailed information about the vulnerability**, including: - - A clear description of the issue. - - Steps to reproduce the vulnerability. - - Potential impact or risk level. - - Any suggested mitigation strategies. -3. **Allow time for assessment and response.** NDTP will review the report and respond within **10 working days** to acknowledge receipt. -4. **Cooperate with NDTP to validate and address the issue.** - -Once a resolution has been identified, NDTP may choose to: - - **Release a patch** as part of the next scheduled update. - - **Issue a security advisory** if the issue is critical. - - **Provide acknowledgments** where appropriate (subject to NDTP’s disclosure policy). - +## Reporting Security Issues + +NDTP takes security seriously and encourages responsible reporting of vulnerabilities. + +If you believe you have found a security vulnerability in this repository, **please do not report it publicly**. Instead, follow the steps below to disclose the issue responsibly. +### **How to Report a Security Issue** +1. **Do not open a public issue on GitHub.** Instead, report security concerns via email to **[ndtp@businessandtrade.gov.uk]**. +2. **Provide detailed information about the vulnerability**, including: + - A clear description of the issue. + - Steps to reproduce the vulnerability. + - Potential impact or risk level. + - Any suggested mitigation strategies. +3. **Allow time for assessment and response.** NDTP will review the report and respond within **10 working days** to acknowledge receipt. +4. **Cooperate with NDTP to validate and address the issue.** + +Once a resolution has been identified, NDTP may choose to: +- **Release a patch** as part of the next scheduled update. +- **Issue a security advisory** if the issue is critical. +- **Provide acknowledgments** where appropriate (subject to NDTP’s disclosure policy). --- - -## Scope - -This security policy applies to: -- All NDTP repositories released as open source. -- Code, configuration files, and infrastructure deployed as part of NDTP’s **Integration Architecture (IA)**. -- **Third-party dependencies** included within NDTP repositories. If you identify a vulnerability in a third-party component that NDTP relies on (e.g., outdated libraries or known security flaws in dependencies), we encourage you to report it. - -Out of scope: -- Issues related to third-party services or software **not used within NDTP repositories**. -- Vulnerabilities in user environments that are unrelated to this repository. -- Unsolicited security testing or penetration testing without NDTP’s explicit permission. +## Scope + +This security policy applies to: +- All NDTP repositories released as open source. +- Code, configuration files, and infrastructure deployed as part of NDTP’s **Integration Architecture (IA)**. +- **Third-party dependencies** included within NDTP repositories. If you identify a vulnerability in a third-party component that NDTP relies on (e.g., outdated libraries + or known security flaws in dependencies), we encourage you to report it. + Out of scope: +- Issues related to third-party services or software **not used within NDTP repositories**. +- Vulnerabilities in user environments that are unrelated to this repository. +- Unsolicited security testing or penetration testing without NDTP’s explicit permission. --- -## Security Best Practices +## Security Best Practices -To help maintain security across NDTP repositories, we follow these principles: -- Dependencies are **scanned and updated regularly** (e.g., using automated tools like Dependabot). -- Sensitive credentials **must not be included** in public repositories. -- Security patches are applied in a timely manner, with priority given to critical vulnerabilities. +To help maintain security across NDTP repositories, we follow these principles: +- Dependencies are **scanned and updated regularly** (e.g., using automated tools like Dependabot). +- Sensitive credentials **must not be included** in public repositories. +- Security patches are applied in a timely manner, with priority given to critical vulnerabilities. --- -**Maintained by the National Digital Twin Programme (NDTP).** - -© 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 NDTP InnerSource Licence – Version 1.0. - -For full licensing terms, see [LICENSE.md](LICENSE.md). +**Maintained by the National Digital Twin Programme (NDTP).** +© 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. +For full licensing terms, see [OGL_LICENSE.md](OGL_LICENSE.md). diff --git a/docker/Dockerfile b/docker/Dockerfile index 6a58c5d..dacf6f9 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,3 +1,10 @@ + +# +# 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. +# + # Build stage FROM maven:3.9.6-eclipse-temurin-21-alpine AS build WORKDIR /build @@ -21,7 +28,7 @@ WORKDIR /app RUN mkdir -p /app/docker /app/logs /app/tmp && chown -R app:app /app # Copy application jar from build stage -COPY --from=build /build/target/management-node-0.0.1.jar /app/app.jar +COPY --from=build /build/target/management-node-0.90.0.jar /app/app.jar RUN chown app:app /app/app.jar # Use non-root user from here on diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev index ff366c7..cc4073a 100644 --- a/docker/Dockerfile-dev +++ b/docker/Dockerfile-dev @@ -1,3 +1,10 @@ + +# +# 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. +# + # Build stage FROM maven:3.9.6-eclipse-temurin-21-alpine AS build WORKDIR /build @@ -10,7 +17,7 @@ COPY src ./src RUN mvn clean package -DskipTests # Runtime stage -FROM eclipse-temurin:21-jdk-alpine +FROM eclipse-temurin:23-jdk-alpine WORKDIR /app @@ -18,7 +25,7 @@ WORKDIR /app 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 --from=build /build/target/management-node-0.90.0.jar /app/app.jar COPY docker/keystore.jks /app/docker/keystore.jks COPY docker/truststore.jks /app/docker/truststore.jks diff --git a/docker/build.sh b/docker/build.sh index 5b3293b..33a1489 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -1,4 +1,10 @@ #!/bin/sh -e +# +# 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. +# + cd .. sudo docker build -f docker/Dockerfile -t ndtp/management-node . diff --git a/docker/keycloak/README.md b/docker/keycloak/README.md index c263779..6625d1c 100644 --- a/docker/keycloak/README.md +++ b/docker/keycloak/README.md @@ -1,9 +1,13 @@ +**Repository:** `management-node` +**Description:** `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.` +**SPDX-License-Identifier:** `Apache-2.0 AND OGL-UK-3.0 ` + + # mTLS with KeyCloak ## Create X.509 certificates - All passwords: _changeit_ ## RootCA diff --git a/docker/keycloak/docker-compose.yml b/docker/keycloak/docker-compose.yml index 7c9a904..a28e706 100644 --- a/docker/keycloak/docker-compose.yml +++ b/docker/keycloak/docker-compose.yml @@ -1,4 +1,9 @@ +# +# 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. +# services: postgres: image: postgres:16.2 diff --git a/docker/keycloak/management-node-realm.json b/docker/keycloak/management-node-realm.json deleted file mode 100644 index 21b96bf..0000000 --- a/docker/keycloak/management-node-realm.json +++ /dev/null @@ -1,3348 +0,0 @@ -{ - "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/docker/keycloak/tofu/Make-Cmds.md b/docker/keycloak/tofu/Make-Cmds.md deleted file mode 100644 index 4f0e654..0000000 --- a/docker/keycloak/tofu/Make-Cmds.md +++ /dev/null @@ -1,114 +0,0 @@ -# Keycloak OpenTofu Make Commands - -This document describes how to use the Makefile in this folder to manage Keycloak resources (realm, clients, client scopes) with OpenTofu. - -Important notes: -- Run these commands from: docker/keycloak/tofu -- Use DIR=. for this repository (the Makefile default DIR=01-global is an upstream default). -- Workspaces (e.g., dev) map to different state and tfvars files. - ---- - -## Quick start - -Initialize OpenTofu, select/create the workspace, and configure the S3 backend: - -```sh -make init WORKSPACE=dev DIR=. -``` - -If your backend file is custom, make sure it matches backends/-backend.tfvars. Example for dev: backends/dev-backend.tfvars. - ---- - -## Plan and apply - -Plan changes (loads terraform.tfvars automatically and tfvars/.tfvars if present): - -```sh -make plan WORKSPACE=dev DIR=. -``` - -Apply the last plan: - -```sh -make apply WORKSPACE=dev DIR=. -``` - -Or apply directly with auto-approve: - -```sh -make apply-auto-approve WORKSPACE=dev DIR=. -``` - ---- - -## Destroy - -Create a destroy plan and destroy resources for the selected workspace: - -```sh -make destroy-plan WORKSPACE=dev DIR=. -make destroy WORKSPACE=dev DIR=. -``` - ---- - -## Validation and formatting - -Format all files and validate configuration: - -```sh -make format -make validate WORKSPACE=dev DIR=. -``` - -Pre-check (fmt -check + validate): - -```sh -make pre-check WORKSPACE=dev DIR=. -``` - -Pre-commit convenience target (runs format and validate): - -```sh -make pre-commit WORKSPACE=dev DIR=. -``` - ---- - -## Upgrade and re-init - -If providers/modules were updated or you need a clean init: - -```sh -make init-upgrade WORKSPACE=dev DIR=. -``` - ---- - -## Variables and files - -- Backend config: backends/-backend.tfvars (e.g., backends/dev-backend.tfvars) -- Per-workspace variables: tfvars/.tfvars (e.g., tfvars/dev.tfvars) -- Default variables: terraform.tfvars - -Key variables (see variables.tf and terraform.tfvars): -- keycloak_url, keycloak_realm, keycloak_client_id, keycloak_username, keycloak_password, keycloak_client_timeout -- management_realm_name, client_access_token_lifespan_seconds -- federator_clients (module input for optional federator clients and role mappings) - ---- - -## Example workflow - -```sh -# From docker/keycloak/tofu -make init WORKSPACE=dev DIR=. -make plan WORKSPACE=dev DIR=. -make apply WORKSPACE=dev DIR=. -``` - -That's it - no AWS regional/global directories are needed here. This Makefile and commands are scoped to the Keycloak OpenTofu configuration in this folder. - - diff --git a/docker/keycloak/tofu/Makefile b/docker/keycloak/tofu/Makefile index 651752d..5a72166 100644 --- a/docker/keycloak/tofu/Makefile +++ b/docker/keycloak/tofu/Makefile @@ -1,3 +1,6 @@ +# 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. + # Default values WORKSPACE ?= dev DIR ?= 01-global # Change this based on the directory you want to work with @@ -16,10 +19,10 @@ apply-auto-approve: @cd $(DIR) && tofu apply -auto-approve destroy-plan: - @cd $(DIR) && tofu plan -destroy -var-file=tfvars/$(WORKSPACE).tfvars + @cd $(DIR) && tofu plan -destroy -var-file=tfvars/$(WORKSPACE).tfvars destroy: - @cd $(DIR) && tofu destroy -var-file=tfvars/$(WORKSPACE).tfvars + @cd $(DIR) && tofu destroy -var-file=tfvars/$(WORKSPACE).tfvars format: tofu fmt --recursive diff --git a/docker/keycloak/tofu/README.md b/docker/keycloak/tofu/README.md index 3ea648a..0670cd3 100644 --- a/docker/keycloak/tofu/README.md +++ b/docker/keycloak/tofu/README.md @@ -1,5 +1,6 @@ -**Repository:** `management-node` -**Description:** `OpenTofu configuration for managing Keycloak (realm, clients, client scopes) used by the Management Node.` +**Repository:** `management-node` +**Description:** `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.` +**SPDX-License-Identifier:** `Apache-2.0 AND OGL-UK-3.0 ` # Overview @@ -11,6 +12,40 @@ This directory contains OpenTofu code to provision and manage Keycloak resources The configuration uses the Keycloak provider and an S3 backend for state (configured via backend tfvars). +Important: Backend selection +- Local runs: set a local backend. +- AWS runs: use the S3 backend as shown in backend.tf. + +Examples: + +Local backend (for running on your machine): +```hcl +terraform { + required_version = ">= 1.6.0" + required_providers { + keycloak = { + source = "keycloak/keycloak" + version = "~> 5.4" + } + } + backend "local" {} +} +``` + +AWS S3 backend (for running in AWS): +```hcl +terraform { + required_version = ">= 1.6.0" + required_providers { + keycloak = { + source = "keycloak/keycloak" + version = "~> 5.4" + } + } + backend "s3" {} +} +``` + --- ## Directory Layout @@ -141,17 +176,15 @@ terraform { } ``` -Then run OpenTofu directly (skip the Makefile init which assumes S3): +Then use the Makefile targets (recommended): ```sh cd docker/keycloak/tofu # Initialize with local backend (no -backend-config needed) -tofu init -# Create/select your workspace -tofu workspace select dev || tofu workspace new dev -# Plan and apply -tofu plan -var-file=tfvars/dev.tfvars -out=tfplan -tofu apply tfplan +make init WORKSPACE=dev DIR=. +# Plan and apply using Makefile targets +make plan WORKSPACE=dev DIR=. +make apply WORKSPACE=dev DIR=. ``` ### Option B: Comment out the S3 backend @@ -185,32 +218,28 @@ terraform { } ``` -Then initialize and apply as in Option A: +Then initialize and apply as in Option A using the Makefile: ```sh cd docker/keycloak/tofu -tofu init -tofu workspace select dev || tofu workspace new dev -tofu plan -var-file=tfvars/dev.tfvars -out=tfplan -tofu apply tfplan +make init WORKSPACE=dev DIR=. +make plan WORKSPACE=dev DIR=. +make apply WORKSPACE=dev DIR=. ``` Notes for local usage: - The state file terraform.tfstate will be created next to backend.tf (and is already ignored by .gitignore). -- If you want to keep using the Makefile for plan/apply, you can: - - Run tofu init manually as shown above (so it uses local backend), and then - - Use the Makefile for subsequent targets, skipping make init, e.g.: - ```sh - cd docker/keycloak/tofu - tofu init - tofu workspace select dev || tofu workspace new dev - make plan WORKSPACE=dev DIR=. - make apply WORKSPACE=dev DIR=. - ``` +- Use the Makefile for all operations; after switching to local backend in backend.tf, simply run: + ```sh + cd docker/keycloak/tofu + make init WORKSPACE=dev DIR=. + make plan WORKSPACE=dev DIR=. + make apply WORKSPACE=dev DIR=. + ``` - To switch back to S3 later, restore the backend "s3" {} block and run: ```sh cd docker/keycloak/tofu - tofu init -reconfigure -backend-config=backends/dev-backend.tfvars + make init WORKSPACE=dev DIR=. ``` --- diff --git a/docker/keycloak/tofu/backend.tf b/docker/keycloak/tofu/backend.tf index 62cb814..7c5305f 100644 --- a/docker/keycloak/tofu/backend.tf +++ b/docker/keycloak/tofu/backend.tf @@ -1,3 +1,5 @@ +# 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. terraform { required_version = ">= 1.6.0" required_providers { @@ -6,5 +8,8 @@ terraform { version = "~> 5.4" } } + backend "s3" {} + #backend "local" {} + } \ No newline at end of file diff --git a/docker/keycloak/tofu/backends/dev-backend.tfvars b/docker/keycloak/tofu/backends/dev-backend.tfvars new file mode 100644 index 0000000..188b6bd --- /dev/null +++ b/docker/keycloak/tofu/backends/dev-backend.tfvars @@ -0,0 +1,8 @@ +# 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. + + +bucket = "5371-2494-4113-state" +key = "keycloak/01-base/dev/terraform.tfstate" +region = "eu-west-2" +encrypt = true diff --git a/docker/keycloak/tofu/backends/dev-backend.tfvars b/docker/keycloak/tofu/backends/dev-backend.tfvars deleted file mode 100644 index 0034bc6..0000000 --- a/docker/keycloak/tofu/backends/dev-backend.tfvars +++ /dev/null @@ -1,4 +0,0 @@ -bucket = "5371-2494-4113-state" -key = "keycloak/01-base/dev/terraform.tfstate" -region = "eu-west-2" -encrypt = true \ No newline at end of file diff --git a/docker/keycloak/tofu/client_scopes.tf b/docker/keycloak/tofu/client_scopes.tf index e1a13aa..c4ab232 100644 --- a/docker/keycloak/tofu/client_scopes.tf +++ b/docker/keycloak/tofu/client_scopes.tf @@ -1,3 +1,6 @@ +# 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. + # Defines custom OpenID client scopes for the Management Node realm # These scopes are referenced by modules/clients via their names @@ -7,12 +10,40 @@ resource "keycloak_openid_client_scope" "federator_consumer" { description = "Client scope for Federator consumer" } +# Ensure Federator Consumer scope resolves audience dynamically (provider v5.4 compatible) +resource "keycloak_generic_protocol_mapper" "federator_consumer_audience_resolve" { + realm_id = keycloak_realm.management-node.id + client_scope_id = keycloak_openid_client_scope.federator_consumer.id + name = "audience resolve" + protocol = "openid-connect" + protocol_mapper = "oidc-audience-resolve-mapper" + + config = { + "access.token.claim" = "true" + "id.token.claim" = "false" + } +} + resource "keycloak_openid_client_scope" "federator_producer" { realm_id = keycloak_realm.management-node.id name = "FEDERATOR_PRODUCER" description = "Client scope for Federator producer" } +# Ensure Federator Producer scope resolves audience dynamically (provider v5.4 compatible) +resource "keycloak_generic_protocol_mapper" "federator_producer_audience_resolve" { + realm_id = keycloak_realm.management-node.id + client_scope_id = keycloak_openid_client_scope.federator_producer.id + name = "audience resolve" + protocol = "openid-connect" + protocol_mapper = "oidc-audience-resolve-mapper" + + config = { + "access.token.claim" = "true" + "id.token.claim" = "false" + } +} + # Scope that adds management-node audience and exposes its client roles in tokens resource "keycloak_openid_client_scope" "management_node_access" { realm_id = keycloak_realm.management-node.id diff --git a/docker/keycloak/tofu/clients.tf b/docker/keycloak/tofu/clients.tf index edc3160..1268c6d 100644 --- a/docker/keycloak/tofu/clients.tf +++ b/docker/keycloak/tofu/clients.tf @@ -1,3 +1,6 @@ +# 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. + resource "keycloak_openid_client" "management_node" { realm_id = keycloak_realm.management-node.id client_id = "management-node" @@ -52,7 +55,7 @@ module "federator_client" { default_client_scopes = [ "FEDERATOR_CONSUMER", "FEDERATOR_PRODUCER", - "MANAGEMENT_NODE_ACCESS", + "MANAGEMENT_NODE_ACCESS" ] # Create roles under this client diff --git a/docker/keycloak/tofu/modules/federator_client/main.tf b/docker/keycloak/tofu/modules/federator_client/main.tf index f85decc..5031469 100644 --- a/docker/keycloak/tofu/modules/federator_client/main.tf +++ b/docker/keycloak/tofu/modules/federator_client/main.tf @@ -1,3 +1,6 @@ +# 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. + resource "keycloak_openid_client" "this" { realm_id = var.realm_id client_id = var.client_id @@ -11,7 +14,7 @@ resource "keycloak_openid_client" "this" { service_accounts_enabled = var.service_accounts_enabled # Per-client JWT access token lifespan (seconds) - access_token_lifespan = var.client_access_token_lifespan_seconds + access_token_lifespan = var.client_access_token_lifespan_seconds # Optional toggles most people like off in machine clients consent_required = var.consent_required @@ -28,17 +31,17 @@ resource "keycloak_openid_client" "this" { # Attach default client scopes if provided resource "keycloak_openid_client_default_scopes" "this" { - count = length(var.default_client_scopes) > 0 ? 1 : 0 - realm_id = var.realm_id - client_id = keycloak_openid_client.this.id + count = length(var.default_client_scopes) > 0 ? 1 : 0 + realm_id = var.realm_id + client_id = keycloak_openid_client.this.id default_scopes = var.default_client_scopes } # Attach optional client scopes if provided resource "keycloak_openid_client_optional_scopes" "this" { - count = length(var.optional_client_scopes) > 0 ? 1 : 0 - realm_id = var.realm_id - client_id = keycloak_openid_client.this.id + count = length(var.optional_client_scopes) > 0 ? 1 : 0 + realm_id = var.realm_id + client_id = keycloak_openid_client.this.id optional_scopes = var.optional_client_scopes } @@ -68,7 +71,7 @@ resource "keycloak_openid_client_service_account_role" "service_account_roles" { # Use only input-derived, stable keys for for_each to avoid plan-time unknowns for_each = { for r in var.service_account_role_ids : "${r.from_client}:${r.name}" => r } - realm_id = var.realm_id + realm_id = var.realm_id # IMPORTANT: this client_id must be the container (owner) of the role) # Resolve the container client's UUID here (arguments may be unknown at plan time, which is OK) client_id = data.keycloak_openid_client.role_containers[each.value.from_client].id diff --git a/docker/keycloak/tofu/modules/federator_client/outputs.tf b/docker/keycloak/tofu/modules/federator_client/outputs.tf index 03ee6d9..9a569fe 100644 --- a/docker/keycloak/tofu/modules/federator_client/outputs.tf +++ b/docker/keycloak/tofu/modules/federator_client/outputs.tf @@ -1,3 +1,7 @@ +# 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. + + output "client_uuid" { description = "UUID of the created Keycloak client (container client_id for role assignments)" value = keycloak_openid_client.this.id diff --git a/docker/keycloak/tofu/modules/federator_client/providers.tf b/docker/keycloak/tofu/modules/federator_client/providers.tf index 6d96d96..72dbb98 100644 --- a/docker/keycloak/tofu/modules/federator_client/providers.tf +++ b/docker/keycloak/tofu/modules/federator_client/providers.tf @@ -1,3 +1,6 @@ +# 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. + terraform { required_version = ">= 1.6.0" required_providers { diff --git a/docker/keycloak/tofu/modules/federator_client/variables.tf b/docker/keycloak/tofu/modules/federator_client/variables.tf index 8787999..4bfa477 100644 --- a/docker/keycloak/tofu/modules/federator_client/variables.tf +++ b/docker/keycloak/tofu/modules/federator_client/variables.tf @@ -1,3 +1,6 @@ +# 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. + variable "realm_id" { description = "Target realm where the client will be created" type = string @@ -117,8 +120,8 @@ variable "optional_client_scopes" { variable "service_account_role_ids" { description = "List of roles to assign to the client's service account. Each item must contain the role name and the source client identifier (client_id string) that owns the role. The module will resolve it to a UUID." type = list(object({ - name = string # role NAME - from_client = string # source client_id (string, e.g., 'management-node' or another client_id) + name = string # role NAME + from_client = string # source client_id (string, e.g., 'management-node' or another client_id) })) default = [] } diff --git a/docker/keycloak/tofu/providers.tf b/docker/keycloak/tofu/providers.tf index e525fde..ceeb124 100644 --- a/docker/keycloak/tofu/providers.tf +++ b/docker/keycloak/tofu/providers.tf @@ -1,3 +1,6 @@ +# 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. + provider "keycloak" { url = var.keycloak_url realm = var.keycloak_realm # manage realms from master diff --git a/docker/keycloak/tofu/realm.tf b/docker/keycloak/tofu/realm.tf index df98101..d32f0ce 100644 --- a/docker/keycloak/tofu/realm.tf +++ b/docker/keycloak/tofu/realm.tf @@ -1,3 +1,7 @@ +# 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. + + resource "keycloak_realm" "management-node" { realm = var.management_realm_name display_name = "Management Node Realm" diff --git a/docker/keycloak/tofu/terraform.tfvars b/docker/keycloak/tofu/terraform.tfvars index 74d51a2..c35b1c9 100644 --- a/docker/keycloak/tofu/terraform.tfvars +++ b/docker/keycloak/tofu/terraform.tfvars @@ -1,3 +1,6 @@ +# 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. + keycloak_url = "http://localhost:8080" keycloak_realm = "master" keycloak_client_id = "admin-cli" diff --git a/docker/keycloak/tofu/tfvars/dev.tfvars b/docker/keycloak/tofu/tfvars/dev.tfvars index e69de29..22c4bbb 100644 --- a/docker/keycloak/tofu/tfvars/dev.tfvars +++ b/docker/keycloak/tofu/tfvars/dev.tfvars @@ -0,0 +1,2 @@ +# 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. diff --git a/docker/keycloak/tofu/variables.tf b/docker/keycloak/tofu/variables.tf index f366be3..31e37fd 100644 --- a/docker/keycloak/tofu/variables.tf +++ b/docker/keycloak/tofu/variables.tf @@ -1,3 +1,6 @@ +# 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. + variable "keycloak_url" { description = "Keycloak base URL" type = string diff --git a/docker/publish.sh b/docker/publish.sh index f0e278e..86b063d 100755 --- a/docker/publish.sh +++ b/docker/publish.sh @@ -1,5 +1,11 @@ #!/bin/sh +# +# 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. +# + AWS_REGION=eu-west-2 AWS_ACCOUNT=$(aws sts get-caller-identity --query Account --output text) diff --git a/pom.xml b/pom.xml index 71ae87f..91a017e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,241 +1,310 @@ - - 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 - - - - - - - - - - - - + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.5 + + + uk.gov.dbt.ndtp.ia.management.node + management-node + 0.90.0 + jar + management-node + Provides Management capabilities over IA Node Net + https://github.com/National-Digital-Twin/management-node + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + A business-friendly OSS license + + - - - 21 - 2025.0.0 - 42.7.7 - 11.10.4 + + + IANodeDevelopers + NDTP@businessandtrade.gov.uk + Department for Business and Trade + https://ndtp.co.uk + + + + scm:git:git@github.com:National-Digital-Twin/management-node.git + scm:git:git@github.com:National-Digital-Twin/management-node.git + https://github.com/National-Digital-Twin/management-node + + + 21 + UTF-8 + 2025.0.0 + 42.7.7 + 11.10.4 + 2.46.1 + 2.11.0 + 2.73.0 + 2.9.1 + 0.8.13 + 3.2.0 + 5.10.0 + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + org.modelmapper + modelmapper + ${modelmapper.version} + + + 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 + ${mockito-junit-jupiter.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito-junit-jupiter.version} + test + - - - - 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 - - - - - - - - - - + + + + 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 + + + + com.diffplug.spotless + spotless-maven-plugin + ${plugin.spotless} + + + + **/*.json + + + 4 + ${plugin.spotless.gson} + + + + + ${plugin.spotless.palantir} + + + + + + 2 + true + false + recommended_2008_06 + + + + + + org.cyclonedx + cyclonedx-maven-plugin + ${plugin.cyclonedx} + + ${project.artifactId}-${project.version}-bom + + + + build-sbom-cyclonedx + + makeAggregateBom + + package + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + **/dto/** + **/entity/** + **/config/** + **/exception/** + **/ManagementNodeApplication.java + + + + + prepare-agent + + prepare-agent + + + + report + + report + + test + + + check + + check + + verify + + 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/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 index 20d2d8b..8581a19 100644 --- 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 @@ -1,3 +1,9 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management; import org.springframework.boot.SpringApplication; @@ -6,8 +12,7 @@ @SpringBootApplication public class ManagementNodeApplication { - public static void main(String[] args) { - SpringApplication.run(ManagementNodeApplication.class, args); - } - + 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 index 237bc3e..dab725e 100644 --- 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 @@ -1,9 +1,16 @@ +/* + * 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. + */ + 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 java.io.IOException; import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; import org.springframework.security.core.Authentication; @@ -12,8 +19,6 @@ 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. @@ -34,26 +39,29 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse 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"); + 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 { @@ -62,33 +70,37 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse 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"); - + 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(); + String clientId = enhancedPrincipal.clientId(); return clientId != null && !clientId.isEmpty() ? clientId : UNKNOWN_CLIENT; } else { - log.warn("Principal is not an instance of EnhancedPrincipal: {}", + log.warn( + "Principal is not an instance of EnhancedPrincipal: {}", principal != null ? principal.getClass().getName() : "null"); } } else { @@ -96,4 +108,4 @@ private String extractClientId(Authentication authentication) { } 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 index 212742f..ee1501c 100644 --- 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 @@ -1,34 +1,40 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.config; +import java.util.Collection; 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 jwt the JWT * @param authorities the collection of granted authorities - * @param principal the custom principal containing subject and clientId + * @param principal the custom principal containing subject and clientId */ - public CustomJwtAuthenticationToken(Jwt jwt, Collection authorities, EnhancedPrincipal principal) { - super(jwt, authorities, principal.getSubject()); + public CustomJwtAuthenticationToken( + Jwt jwt, Collection authorities, EnhancedPrincipal principal) { + super(jwt, authorities, principal.subject()); 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 index b138702..a103a47 100644 --- 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 @@ -1,5 +1,13 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.config; +import java.util.*; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.convert.converter.Converter; @@ -21,29 +29,26 @@ 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 @@ -51,7 +56,6 @@ */ @Component @Slf4j - public class KeycloakJwtAuthenticationConverter implements Converter { // Constants for claim names private static final String CLAIM_AZP = "azp"; @@ -72,7 +76,8 @@ public class KeycloakJwtAuthenticationConverter implements Converter response = restTemplate.postForEntity(introspectionUri, requestEntity, JwtToken.class); + ResponseEntity response = + restTemplate.postForEntity(introspectionUri, requestEntity, JwtToken.class); // Parse the response JwtToken introspectionData = response.getBody(); @@ -160,7 +166,10 @@ public AbstractAuthenticationToken convert(Jwt jwt) { } 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()); + 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); @@ -168,7 +177,10 @@ public AbstractAuthenticationToken convert(Jwt jwt) { } 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()); + 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); @@ -233,7 +245,8 @@ private Collection extractAuthoritiesFromIntrospection(JwtToke 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)); + authorities.add(new SimpleGrantedAuthority( + ROLE_PREFIX + resource + RESOURCE_ROLE_SEPARATOR + role)); }); } }); @@ -242,7 +255,8 @@ private Collection extractAuthoritiesFromIntrospection(JwtToke 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); + throw new ResourceAccessParsingException( + "Failed to parse resource access from introspection data", e, clientId); } return authorities; @@ -303,15 +317,21 @@ private Collection processResourceRoles(String resourceName, M 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()); + 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) { @@ -323,7 +343,11 @@ private Collection extractAuthorities(Jwt jwt) { 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))))); + 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) { @@ -334,4 +358,4 @@ private Collection extractAuthorities(Jwt jwt) { 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 index b27712f..136b82d 100644 --- 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 @@ -1,3 +1,9 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.config; import org.modelmapper.ModelMapper; @@ -24,4 +30,4 @@ public ModelMapper 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 index 4882439..2ee8bd3 100644 --- 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 @@ -1,3 +1,9 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.config; import org.springframework.context.annotation.Bean; @@ -7,7 +13,6 @@ 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 @@ -15,32 +20,30 @@ public class SecurityConfig { private final KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter; private final ClientIdMdcFilter clientIdMdcFilter; - - public SecurityConfig(KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter, - 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); - + 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 index 000971d..6fc3147 100644 --- 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 @@ -1,3 +1,9 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.config; import jakarta.annotation.PostConstruct; @@ -28,11 +34,11 @@ public class SslPropertyInitializer { @PostConstruct public void init() { System.setProperty("javax.net.ssl.keyStore", keyStore); - System.setProperty("javax.net.ssl.keyStorePassword",keyStorePassword); + 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 index 71ebf3d..db99de1 100644 --- 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 @@ -1,5 +1,12 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.controller.v1; +import java.util.Optional; import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -12,42 +19,35 @@ 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()); - } - - + 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.clientId(), 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.clientId(), consumerId); + + return configurationProvider.getConsumerConfigByClientId( + principal.clientId(), 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 index f72b20d..fa798c9 100644 --- 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 @@ -1,3 +1,9 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.converter; import java.util.List; @@ -37,9 +43,7 @@ default List toDtoList(List entities) { if (entities == null) { return List.of(); } - return entities.stream() - .map(this::toDto) - .collect(Collectors.toList()); + return entities.stream().map(this::toDto).collect(Collectors.toList()); } /** @@ -52,8 +56,6 @@ default List toEntityList(List dtos) { if (dtos == null) { return List.of(); } - return dtos.stream() - .map(this::toEntity) - .collect(Collectors.toList()); + 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 index 698c377..254dc50 100644 --- 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 @@ -1,10 +1,16 @@ +/* + * 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. + */ + 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.entity.Organisation; import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.OrganisationRepository; /** @@ -63,11 +69,11 @@ public Consumer toEntity(ConsumerDTO dto) { // Set the organisation if orgId is provided if (dto.getOrgId() != null) { - Organisation organisation = organisationRepository.findById(dto.getOrgId()) - .orElse(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 index fd08df7..126ebb3 100644 --- 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 @@ -1,16 +1,21 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.converter.impl; +import java.util.ArrayList; +import java.util.List; 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.entity.Product; 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. */ @@ -24,10 +29,10 @@ public class OrganisationProducerConverter implements EntityDtoConverter - dto.getDataProviders().add(productConverter.toDto(dataProvider))); + entity.getProducts() + .forEach(dataProvider -> dto.getDataProviders().add(productConverter.toDto(dataProvider))); } - + return dto; } @@ -89,11 +94,11 @@ public Producer toEntity(ProducerDTO dto) { // Set the organisation if orgId is provided if (dto.getOrgId() != null) { - Organisation organisation = organisationRepository.findById(dto.getOrgId()) - .orElse(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<>(); @@ -113,4 +118,4 @@ public Producer toEntity(ProducerDTO dto) { 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 index 2b9d96e..c096839 100644 --- 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 @@ -1,16 +1,21 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.converter.impl; +import java.util.ArrayList; +import java.util.List; 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.entity.Product; 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. */ @@ -24,10 +29,9 @@ public class ProducerConverter implements EntityDtoConverter - dto.getDataProviders().add(productConverter.toDto(dataProvider))); + entity.getProducts() + .forEach(dataProvider -> dto.getDataProviders().add(productConverter.toDto(dataProvider))); } - + return dto; } @@ -89,11 +93,11 @@ public Producer toEntity(ProducerDTO dto) { // Set the organisation if orgId is provided if (dto.getOrgId() != null) { - Organisation organisation = organisationRepository.findById(dto.getOrgId()) - .orElse(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<>(); @@ -113,4 +117,4 @@ public Producer toEntity(ProducerDTO dto) { 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 index 0650c28..f39badf 100644 --- 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 @@ -1,3 +1,9 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.converter.impl; import org.springframework.stereotype.Component; @@ -45,16 +51,16 @@ public ProductConsumer toEntity(ProductConsumerDTO dto) { } 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 index 9427d5e..bc10ad4 100644 --- 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 @@ -1,10 +1,16 @@ +/* + * 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. + */ + 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.entity.Product; import uk.gov.dbt.ndtp.ia.node.management.persistency.repository.ProducerRepository; /** @@ -63,11 +69,10 @@ public Product toEntity(ProductDTO dto) { // Set the producer if producerId is provided if (dto.getProducerId() != null) { - Producer producer = producerRepository.findById(dto.getProducerId()) - .orElse(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 index 7a5dd4a..cc707a2 100644 --- 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 @@ -1,3 +1,9 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.exception; import lombok.Getter; @@ -11,33 +17,32 @@ public class AuthenticationProcessingException extends RuntimeException { /** * -- GETTER -- - * Gets the client ID associated with this exception. + * 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 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 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 index e090f37..7441884 100644 --- 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 @@ -1,3 +1,9 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.exception; import lombok.AllArgsConstructor; @@ -12,19 +18,19 @@ @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 index 9a4403c..d53c1f8 100644 --- 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 @@ -1,3 +1,9 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.exception; /** @@ -6,25 +12,25 @@ * 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 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 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 index bb20257..856d60b 100644 --- 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 @@ -1,3 +1,9 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.exception; /** @@ -5,25 +11,25 @@ * 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 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 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 index 175586b..8cf314f 100644 --- 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 @@ -1,3 +1,9 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.exception; /** @@ -6,25 +12,25 @@ * 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 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 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 index a351695..12d1470 100644 --- 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 @@ -1,5 +1,12 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.exception.handlers; +import java.util.UUID; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -9,8 +16,6 @@ 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 @@ -19,10 +24,10 @@ @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { - + /** * Generates a unique error ID for tracking and correlation. - * + * * @return a unique UUID string */ private String generateErrorId() { @@ -31,71 +36,63 @@ private String generateErrorId() { /** * Handles AuthenticationProcessingException and its subclasses. - * - * @param ex the exception + * + * @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 - ); - + 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 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) { - + 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 - ); - + 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 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) { - + 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 - ); - + + 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 index 449a5ce..93c566b 100644 --- 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 @@ -1,3 +1,9 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.model.dto; import java.util.List; @@ -8,6 +14,6 @@ @Getter public class ConsumerConfigDTO { - private final String clientId; - private final List producers; + 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 index eab5b78..9f10db3 100644 --- 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 @@ -1,11 +1,13 @@ +/* + * 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. + */ + 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 lombok.*; /** * DTO for consumerId entity. @@ -18,8 +20,11 @@ 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 index 447ebb5..e2a2c57 100644 --- 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 @@ -1,10 +1,15 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.model.dto; +import java.util.List; import lombok.Builder; import lombok.Getter; -import java.util.List; - @Builder @Getter public class ProducerConfigDTO { 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 index bcff794..658dd9a 100644 --- 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 @@ -1,15 +1,16 @@ +/* + * 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. + */ + 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; +import lombok.*; /** * DTO for OrganisationProducer entity. @@ -20,16 +21,20 @@ @NoArgsConstructor @AllArgsConstructor public class ProducerDTO { + private final List dataProviders = new ArrayList<>(); + @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 index ba65767..03d904c 100644 --- 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 @@ -1,13 +1,14 @@ -package uk.gov.dbt.ndtp.ia.node.management.model.dto; +/* + * 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. + */ -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +package uk.gov.dbt.ndtp.ia.node.management.model.dto; import java.math.BigDecimal; import java.sql.Timestamp; +import lombok.*; /** * DTO for ConsumerAllowedDataProvider entity. @@ -22,4 +23,4 @@ public class ProductConsumerDTO { 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 index 92e6efc..4c63020 100644 --- 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 @@ -1,13 +1,14 @@ +/* + * 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. + */ + 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; +import lombok.*; /** * DTO for OrganisationDataProvider entity. @@ -20,9 +21,12 @@ 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 index 0aa47e9..2eaf680 100644 --- 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 @@ -1,42 +1,28 @@ -package uk.gov.dbt.ndtp.ia.node.management.model.jwt; +/* + * 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. + */ -import lombok.Getter; +package uk.gov.dbt.ndtp.ia.node.management.model.jwt; import java.io.Serial; import java.io.Serializable; /** * Custom Principal object that includes clientId information from the JWT. + * + * @param subject -- GETTER -- + * Get the subject (user identifier) + * @param clientId -- GETTER -- + * Get the client ID */ -@Getter -public class EnhancedPrincipal implements Serializable { +public record EnhancedPrincipal(String subject, String clientId) 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 + '\'' + - '}'; + 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 index 0752db2..ef282b9 100644 --- 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 @@ -1,14 +1,18 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.model.jwt; import com.fasterxml.jackson.annotation.JsonFormat; +import java.util.List; +import java.util.Map; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import com.fasterxml.jackson.annotation.JsonFormat; - -import java.util.List; -import java.util.Map; /** * Represents the structure of a JWT token. @@ -23,8 +27,10 @@ public class JwtToken { private Long iat; private String jti; private String iss; + @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) private List aud; + private String sub; private String typ; private String azp; @@ -46,4 +52,4 @@ public class JwtToken { 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 index ac7fba0..10d7e22 100644 --- 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 @@ -1,11 +1,16 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.persistency.entity; import jakarta.persistence.*; +import java.util.List; import lombok.Getter; import lombok.Setter; -import java.util.List; - @Getter @Setter @Entity @@ -26,7 +31,7 @@ public class Consumer { @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; + @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 index be5d7c8..f2ac4dd 100644 --- 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 @@ -1,3 +1,9 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.persistency.entity; import jakarta.persistence.*; @@ -16,5 +22,4 @@ public class Organisation { @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 index 8289aa3..14e9fa1 100644 --- 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 @@ -1,11 +1,16 @@ +/* + * 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. + */ + 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; +import lombok.Getter; +import lombok.Setter; @Getter @Setter @@ -42,6 +47,6 @@ public class Producer { @Column(name = "idp_client_id", nullable = false, length = 50) private String idpClientId; - @OneToMany(mappedBy = "producer", fetch = FetchType.LAZY) - private List products; + @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 index b08748f..1492166 100644 --- 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 @@ -1,11 +1,16 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.persistency.entity; import jakarta.persistence.*; +import java.util.List; import lombok.Getter; import lombok.Setter; -import java.util.List; - @Getter @Setter @Entity @@ -21,16 +26,12 @@ public class Product { @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; + @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 index f5d1ce1..8799d9f 100644 --- 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 @@ -1,14 +1,19 @@ +/* + * 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. + */ + 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; +import lombok.Getter; +import lombok.Setter; @Getter @Setter @@ -23,6 +28,4 @@ public class ProductConsumer { @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 index 2e8004f..601fd8a 100644 --- 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 @@ -1,14 +1,19 @@ +/* + * 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. + */ + 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; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.Hibernate; @Getter @Setter @@ -16,6 +21,7 @@ public class ProductConsumerId implements Serializable { @Serial private static final long serialVersionUID = -1247742635043749804L; + @Column(name = "product_id", nullable = false) private Long productId; @@ -27,13 +33,11 @@ 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); + 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 index 29cf146..fd17a6f 100644 --- 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 @@ -1,5 +1,12 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.persistency.repository; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -7,8 +14,6 @@ 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 { @@ -17,4 +22,4 @@ public interface ConsumerProviderRepository extends JpaRepository 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 index 838f1d5..979f9f2 100644 --- 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 @@ -1,12 +1,17 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.persistency.repository; +import java.util.List; 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 { @@ -14,5 +19,4 @@ public interface ConsumerRepository extends JpaRepository { @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 index 299e60b..208f537 100644 --- 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 @@ -1,3 +1,9 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.persistency.repository; import org.springframework.data.jpa.repository.JpaRepository; @@ -5,5 +11,4 @@ import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Organisation; @Repository -public interface OrganisationRepository extends JpaRepository { -} \ No newline at end of file +public interface OrganisationRepository extends JpaRepository {} 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 index a6210ee..c9dcf15 100644 --- 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 @@ -1,12 +1,17 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.persistency.repository; +import java.util.List; 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 { @@ -14,5 +19,5 @@ public interface ProducerRepository extends JpaRepository { 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 + List findByIdpClientId(String idpClientId); +} 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 index 14a5a09..5d3f057 100644 --- 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 @@ -1,12 +1,17 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.persistency.repository; +import java.util.List; 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 { @@ -15,5 +20,4 @@ public interface ProductRepository extends JpaRepository { @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 index a6aae47..918b5e5 100644 --- 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 @@ -1,10 +1,15 @@ -package uk.gov.dbt.ndtp.ia.node.management.service.data; +/* + * 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. + */ -import uk.gov.dbt.ndtp.ia.node.management.model.dto.ConsumerDTO; +package uk.gov.dbt.ndtp.ia.node.management.service.data; import java.util.List; import java.util.Map; import java.util.Optional; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ConsumerDTO; /** * Service interface for managing ConsumerId entities. @@ -18,8 +23,6 @@ public interface ConsumerService { */ Optional findById(Long id); - - /** * Find an ConsumerId by its IDP client ID. * @@ -28,13 +31,12 @@ public interface ConsumerService { */ 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 + * 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 index 8bdf87c..8452bcb 100644 --- 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 @@ -1,10 +1,12 @@ -package uk.gov.dbt.ndtp.ia.node.management.service.data; +/* + * 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. + */ +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 +public interface OrganisationService {} 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 index 810cc3d..6f5de39 100644 --- 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 @@ -1,23 +1,26 @@ -package uk.gov.dbt.ndtp.ia.node.management.service.data; +/* + * 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. + */ -import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProducerDTO; +package uk.gov.dbt.ndtp.ia.node.management.service.data; import java.util.List; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProducerDTO; /** * 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 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 index 79b8341..8ccf6f4 100644 --- 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 @@ -1,14 +1,19 @@ -package uk.gov.dbt.ndtp.ia.node.management.service.data; +/* + * 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. + */ -import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProductConsumerDTO; +package uk.gov.dbt.ndtp.ia.node.management.service.data; import java.util.List; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProductConsumerDTO; /** * Service interface for managing ConsumerAllowedDataProvider entities. */ public interface ProductConsumerService { - + /** * Find all ConsumerAllowedDataProvider entities by consumer ID. * @@ -18,5 +23,4 @@ public interface ProductConsumerService { 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 index a0ef2ee..edcbfec 100644 --- 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 @@ -1,14 +1,19 @@ -package uk.gov.dbt.ndtp.ia.node.management.service.data; +/* + * 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. + */ -import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProductDTO; +package uk.gov.dbt.ndtp.ia.node.management.service.data; import java.util.List; +import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProductDTO; /** * Service interface for managing OrganisationDataProvider entities. */ public interface ProductService { - + /** * Retrieves a list of OrganisationDataProviderDTO objects by their IDs. * @@ -17,7 +22,6 @@ public interface ProductService { */ List getProductsByIds(List ids); - /** * Retrieves a list of DataProviderDTO objects associated with the specified producer IDs. * @@ -25,4 +29,4 @@ public interface ProductService { * @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 index 420734a..bbcb8bf 100644 --- 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 @@ -1,5 +1,15 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; 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; @@ -7,11 +17,6 @@ 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. */ @@ -24,11 +29,10 @@ public class ConsumerServiceImpl implements ConsumerService { /** * Constructor-based dependency injection. * - * @param consumerRepository the organisation consumer repository + * @param consumerRepository the organisation consumer repository * @param consumerIdConverter the converter for entity-to-DTO conversion */ - public ConsumerServiceImpl(ConsumerRepository consumerRepository, - ConsumerConverter consumerIdConverter) { + public ConsumerServiceImpl(ConsumerRepository consumerRepository, ConsumerConverter consumerIdConverter) { this.consumerRepository = consumerRepository; this.consumerIdConverter = consumerIdConverter; } @@ -48,13 +52,12 @@ public List findByIdpClientId(String 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()))); - } + @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 index 9c1de38..4d38476 100644 --- 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 @@ -1,3 +1,9 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; import org.springframework.stereotype.Service; @@ -20,6 +26,4 @@ public class OrganisationServiceImpl implements OrganisationService { 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 index d5b41bf..7316cae 100644 --- 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 @@ -1,5 +1,12 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; +import java.util.List; 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; @@ -7,8 +14,6 @@ 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. */ @@ -21,24 +26,25 @@ public class ProducerServiceImpl implements ProducerService { /** * Constructor-based dependency injection. * - * @param producerRepository the organisation producer repository + * @param producerRepository the organisation producer repository * @param organisationProducerConverter the converter for entity-to-DTO conversion */ - public ProducerServiceImpl(ProducerRepository producerRepository, - OrganisationProducerConverter organisationProducerConverter) { + public ProducerServiceImpl( + ProducerRepository producerRepository, OrganisationProducerConverter organisationProducerConverter) { this.producerRepository = producerRepository; this.organisationProducerConverter = organisationProducerConverter; - } - /** {@inheritDoc} */ - @Override - public List getProducersByIds(List producerIds) { - List producers = producerRepository.findByIds(producerIds); + /** + * {@inheritDoc} + */ + @Override + public List getProducersByIds(List producerIds) { + List producers = producerRepository.findByIds(producerIds); - // Convert entities to DTOs using the converter - return organisationProducerConverter.toDtoList(producers); - } + // Convert entities to DTOs using the converter + return organisationProducerConverter.toDtoList(producers); + } @Override public List getProducersByClientId(String clientId) { 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 index aec29da..60ca6d7 100644 --- 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 @@ -1,5 +1,12 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; +import java.util.List; 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; @@ -7,8 +14,6 @@ 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. */ @@ -22,11 +27,10 @@ public class ProductConsumerServiceImpl implements ProductConsumerService { * Constructor-based dependency injection. * * @param consumerProviderRepository the consumer allowed data provider repository - * @param productConsumerConverter the converter for entity-to-DTO conversion + * @param productConsumerConverter the converter for entity-to-DTO conversion */ public ProductConsumerServiceImpl( - ConsumerProviderRepository consumerProviderRepository, - ProductConsumerConverter productConsumerConverter) { + ConsumerProviderRepository consumerProviderRepository, ProductConsumerConverter productConsumerConverter) { this.consumerProviderRepository = consumerProviderRepository; this.consumerProviderConverter = productConsumerConverter; } @@ -45,5 +49,4 @@ 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 index 5c7033c..2762633 100644 --- 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 @@ -1,5 +1,13 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; +import java.util.List; +import java.util.Optional; 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; @@ -7,9 +15,6 @@ 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. */ @@ -23,11 +28,9 @@ public class ProductServiceImpl implements ProductService { * Constructor-based dependency injection. * * @param productRepository the organisation data provider repository - * @param productConverter the converter for entity-to-DTO conversion + * @param productConverter the converter for entity-to-DTO conversion */ - public ProductServiceImpl( - ProductRepository productRepository, - ProductConverter productConverter) { + public ProductServiceImpl(ProductRepository productRepository, ProductConverter productConverter) { this.productRepository = productRepository; this.productConverter = productConverter; } @@ -40,11 +43,10 @@ public List getProductsByIds(List ids) { if (ids == null || ids.isEmpty()) { return List.of(); } - List dataProviders = - productRepository.findByIds(ids); + List dataProviders = productRepository.findByIds(ids); return Optional.ofNullable(dataProviders) - .map(productConverter::toDtoList) - .orElse(List.of()); + .map(productConverter::toDtoList) + .orElse(List.of()); } /** @@ -57,5 +59,4 @@ public List getProductsByProducerIds(List producerIds) { .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 index ae26fe2..374fcf1 100644 --- 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 @@ -1,10 +1,15 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.service.providers.configuration; +import java.util.Optional; 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. *

@@ -12,7 +17,7 @@ * 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 { @@ -20,23 +25,22 @@ 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 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. + * @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 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. + * @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 index 1ca41ca..e01055b 100644 --- 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 @@ -1,3 +1,9 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.service.providers.configuration; import java.math.BigDecimal; @@ -8,222 +14,216 @@ 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; +import uk.gov.dbt.ndtp.ia.node.management.service.data.ProductConsumerService; +import uk.gov.dbt.ndtp.ia.node.management.service.data.ProductService; @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(); + 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; + } + + private static boolean isValidGrantedTs(Timestamp grantedTs, BigDecimal validity) { + return grantedTs != null + && grantedTs + .toInstant() + .plus(java.time.Duration.ofDays(validity.longValue())) + .isAfter(Instant.now()); + } + + @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(); } - - 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(); + + /** + * 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; } - - 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); + + /** + * 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); } - - 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); - } + + /** + * 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(); } - } - - /** - * 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<>()); + + private List getValidDataProviders(List consumers) { + return consumers.stream() + .map(consumer -> consumerAllowedDataProvidersService.findByConsumerId(consumer.getId())) + .flatMap(List::stream) + .filter(this::isValidProvider) + .toList(); } - - // 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); - }); - } + /** + * 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); + } - private boolean isValidProvider(ProductConsumerDTO 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); + }); + } - if (provider.getValidity() == null || provider.getValidity().equals(BigDecimal.ZERO)) - return true; + private boolean isValidProvider(ProductConsumerDTO provider) { - return isValidGrantedTs(provider.getGrantedTs(), provider.getValidity()); - } + if (provider.getValidity() == null || provider.getValidity().equals(BigDecimal.ZERO)) return true; - private static boolean isValidGrantedTs(Timestamp grantedTs, BigDecimal validity) { - return grantedTs != null - && grantedTs - .toInstant() - .plus(java.time.Duration.ofDays(validity.longValue())) - .isAfter(Instant.now()); - } + return isValidGrantedTs(provider.getGrantedTs(), provider.getValidity()); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8f68c1e..48c3c3d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,12 +5,12 @@ spring: oauth2: resourceserver: jwt: - issuer-uri: https://localhost:8443/realms/management-node - jwk-set-uri: https://localhost:8443/realms/management-node/protocol/openid-connect/certs + issuer-uri: https://localhost:8443/realms/mng-node + jwk-set-uri: https://localhost:8443/realms/mng-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 + introspection-uri: https://localhost:8443/realms/mng-node/protocol/openid-connect/token/introspect client-secret: client-id: flyway: @@ -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: @@ -32,21 +32,21 @@ spring: # Server configuration server: - port: 8443 + port: 8090 ssl: key-alias: localhost - key-store: keystore.jks + key-store: /home/developer/Downloads/managementNode/docker/keystore.jks key-store-type: JKS - key-store-password: - trust-store: truststore.jks - trust-store-password: + key-store-password: changeit + trust-store: /home/developer/Downloads/managementNode/docker/truststore.jks + trust-store-password: changeit trust-store-type: JKS client-auth: need enabled: true application: client: - key-store: client-keystore.jks - keyStorePassword: + key-store: /home/developer/Downloads/managementNode/docker/keystore.jks + keyStorePassword: changeit keyStoreType: JKS # Actuator Configuration diff --git a/src/main/resources/db/migration/V20250728142253__intial_database_tables.sql b/src/main/resources/db/migration/V20250728142253__intial_database_tables.sql index 96dfea3..6155acb 100644 --- a/src/main/resources/db/migration/V20250728142253__intial_database_tables.sql +++ b/src/main/resources/db/migration/V20250728142253__intial_database_tables.sql @@ -1,9 +1,15 @@ +/* + * 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. + */ + create table organisation ( - id bigserial + id bigserial constraint pk_organisation primary key, - name varchar(150) not null + name varchar(150) not null ); @@ -43,12 +49,12 @@ create table consumer create table product ( - id bigserial not null + id bigserial not null constraint pk_3 primary key, - name varchar(50) not null, - topic varchar(150) not null, - producer_id bigint not null + name varchar(50) not null, + topic varchar(150) not null, + producer_id bigint not null constraint fk_2 references producer ); diff --git a/src/main/resources/db/samples/V20250728152300__sample_data.sql b/src/main/resources/db/samples/V20250728152300__sample_data.sql index 5a0de90..9111092 100644 --- a/src/main/resources/db/samples/V20250728152300__sample_data.sql +++ b/src/main/resources/db/samples/V20250728152300__sample_data.sql @@ -1,26 +1,38 @@ +/* + * 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. + */ + + -- 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)'); +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 ('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'); +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'); - +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) +INSERT INTO consumer (name, org_id, idp_client_id) VALUES ('ENV-CONSUMER-1', (select id from organisation where name like '%ENV%'), 'FEDERATOR_ENV'); @@ -33,29 +45,40 @@ VALUES ('HEG-CONSUMER-1', (select id from organisation where name like '%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%'));; +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%'));; +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%'));; +/* + * 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. + */ + -- 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); +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); +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); +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 index 85e8221..7e7663a 100644 --- 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 @@ -1,3 +1,9 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management; import org.junit.jupiter.api.Test; @@ -6,8 +12,6 @@ @SpringBootTest class ManagementNodeApplicationTests { - @Test - void contextLoads() { - } - + @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 index 5073d33..381b682 100644 --- 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 @@ -1,5 +1,18 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.config; +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; + +import java.time.Instant; +import java.util.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -17,14 +30,6 @@ 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. */ @@ -42,10 +47,13 @@ class KeycloakJwtAuthenticationConverterExceptionTest { @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, + "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"); @@ -60,11 +68,11 @@ void setUp() { 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<>(); @@ -72,16 +80,11 @@ void setUp() { 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 - ); - + "token-value", Instant.ofEpochSecond(1753575765), Instant.ofEpochSecond(1753576065), headers, claims); + // Inject mock RestTemplate ReflectionTestUtils.setField(converter, "restTemplate", restTemplate); } @@ -90,25 +93,22 @@ void setUp() { 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")); - + 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()); + assertEquals("86a41a8a-ab2e-465e-8b48-a09d3275f842", principal.subject()); + assertEquals("management-node", principal.clientId()); } - + @Test void convert_withMalformedIntrospectionResponse_shouldFallbackToJwtParsing() { // Arrange @@ -117,80 +117,71 @@ void convert_withMalformedIntrospectionResponse_shouldFallbackToJwtParsing() { 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); - + 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()); + assertEquals("86a41a8a-ab2e-465e-8b48-a09d3275f842", principal.subject()); + assertEquals("management-node", principal.clientId()); } - + @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); - + 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()); + assertEquals("86a41a8a-ab2e-465e-8b48-a09d3275f842", principal.subject()); + assertEquals("management-node", principal.clientId()); } - + @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); - + 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()); + assertEquals("86a41a8a-ab2e-465e-8b48-a09d3275f842", principal.subject()); + assertEquals("management-node", principal.clientId()); } -} \ 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 index a7ef272..21e8c6b 100644 --- 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 @@ -1,5 +1,18 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.config; +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; + +import java.time.Instant; +import java.util.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -18,14 +31,6 @@ 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 { @@ -41,10 +46,13 @@ class KeycloakJwtAuthenticationConverterTest { @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, + "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"); @@ -59,52 +67,47 @@ void setUp() { 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 - ); - + "token-value", Instant.ofEpochSecond(1753575765), Instant.ofEpochSecond(1753576065), headers, claims); + // Create mock introspection response mockIntrospectionResponse = new HashMap<>(); mockIntrospectionResponse.put("active", true); @@ -122,28 +125,29 @@ void setUp() { 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); + 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) { @@ -155,258 +159,240 @@ private JwtToken createJwtTokenFromMap(Map map) { } }); } - + // 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(); + .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()); - + assertEquals("86a41a8a-ab2e-465e-8b48-a09d3275f842", principal.subject()); + assertEquals("management-node", principal.clientId()); + 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" - ); - + 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)); + .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 - ); - + "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")); - + .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" - + assertEquals("test-subject", principal.subject()); + assertEquals("unknown", principal.clientId()); // 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 - ); - + "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")); - + .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" - + assertEquals("test-subject", principal.subject()); + assertEquals("unknown", principal.clientId()); // 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 - ); - + "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")); - + .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" + assertEquals("test-subject", principal.subject()); + assertEquals("unknown", principal.clientId()); // 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() - ); - + 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()); - + assertEquals("86a41a8a-ab2e-465e-8b48-a09d3275f842", principal.subject()); + assertEquals("management-node", principal.clientId()); + // 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" - ); - + 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)); + .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")); - + 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()); + assertEquals("86a41a8a-ab2e-465e-8b48-a09d3275f842", principal.subject()); + assertEquals("management-node", principal.clientId()); } -} \ 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 index 6d5dda1..e75ac25 100644 --- 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 @@ -1,5 +1,19 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.controller.v1; +import static org.mockito.ArgumentMatchers.any; +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; + +import java.util.ArrayList; +import java.util.Collections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -14,16 +28,6 @@ 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 { @@ -44,8 +48,7 @@ class ConfigurationControllerTest { @BeforeEach void setUp() { - mockMvc = MockMvcBuilders.standaloneSetup(configurationController) - .build(); + mockMvc = MockMvcBuilders.standaloneSetup(configurationController).build(); // Set up producer config ProducerDTO producerDTO = ProducerDTO.builder() @@ -53,7 +56,7 @@ void setUp() { .name("Test Producer") .active(true) .build(); - + producerConfigDTO = ProducerConfigDTO.builder() .clientId(CLIENT_ID) .producers(Collections.singletonList(producerDTO)) @@ -74,16 +77,14 @@ void setUp() { // 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); + when(configurationProvider.getProducerConfigByClientId(any(), any())).thenReturn(producerConfigDTO); // Act & Assert - mockMvc.perform(get("/api/v1/configuration/producer") - .contentType(MediaType.APPLICATION_JSON)) + mockMvc.perform(get("/api/v1/configuration/producer").contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.clientId").value(CLIENT_ID)); } @@ -91,8 +92,7 @@ void getProducerConfigurations_shouldReturnConfig() throws Exception { @Test void getProducerConfigurations_withProducerId_shouldReturnFilteredConfig() throws Exception { // Arrange - when(configurationProvider.getProducerConfigByClientId(any(), any())) - .thenReturn(producerConfigDTO); + when(configurationProvider.getProducerConfigByClientId(any(), any())).thenReturn(producerConfigDTO); // Act & Assert mockMvc.perform(get("/api/v1/configuration/producer") @@ -105,12 +105,10 @@ void getProducerConfigurations_withProducerId_shouldReturnFilteredConfig() throw @Test void getConsumerConfigurations_shouldReturnConfig() throws Exception { // Arrange - when(configurationProvider.getConsumerConfigByClientId(any(), any())) - .thenReturn(consumerConfigDTO); + when(configurationProvider.getConsumerConfigByClientId(any(), any())).thenReturn(consumerConfigDTO); // Act & Assert - mockMvc.perform(get("/api/v1/configuration/consumer") - .contentType(MediaType.APPLICATION_JSON)) + mockMvc.perform(get("/api/v1/configuration/consumer").contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.clientId").value(CLIENT_ID)); } @@ -118,8 +116,7 @@ void getConsumerConfigurations_shouldReturnConfig() throws Exception { @Test void getConsumerConfigurations_withConsumerId_shouldReturnFilteredConfig() throws Exception { // Arrange - when(configurationProvider.getConsumerConfigByClientId(any(), any())) - .thenReturn(consumerConfigDTO); + when(configurationProvider.getConsumerConfigByClientId(any(), any())).thenReturn(consumerConfigDTO); // Act & Assert mockMvc.perform(get("/api/v1/configuration/consumer") @@ -128,4 +125,4 @@ void getConsumerConfigurations_withConsumerId_shouldReturnFilteredConfig() throw .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 index 397a230..d47ee7b 100644 --- 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 @@ -1,5 +1,15 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.converter.impl; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -7,15 +17,10 @@ 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.entity.Organisation; 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 { @@ -28,7 +33,7 @@ class ConsumerConverterTest { 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"; @@ -41,7 +46,7 @@ void setUp() { organisation = new Organisation(); organisation.setId(orgId); organisation.setName(orgName); - + // Create test entity entity = new Consumer(); entity.setId(consumerId); @@ -83,7 +88,7 @@ void toDto_withValidEntity_shouldReturnCorrectDTO() { void toDto_withNullOrg_shouldReturnDTOWithNullOrgId() { // Arrange entity.setOrg(null); - + // Act ConsumerDTO result = converter.toDto(entity); @@ -108,7 +113,7 @@ void toEntity_withNullDTO_shouldReturnNull() { void toEntity_withValidDTO_shouldReturnCorrectEntity() { // Arrange when(organisationRepository.findById(orgId)).thenReturn(Optional.of(organisation)); - + // Act Consumer result = converter.toEntity(dto); @@ -120,7 +125,7 @@ void toEntity_withValidDTO_shouldReturnCorrectEntity() { assertNotNull(result.getOrg()); assertEquals(orgId, result.getOrg().getId()); assertEquals(orgName, result.getOrg().getName()); - + // Verify verify(organisationRepository, times(1)).findById(orgId); } @@ -129,7 +134,7 @@ void toEntity_withValidDTO_shouldReturnCorrectEntity() { void toEntity_withNullOrgId_shouldReturnEntityWithNullOrg() { // Arrange dto.setOrgId(null); - + // Act Consumer result = converter.toEntity(dto); @@ -139,7 +144,7 @@ void toEntity_withNullOrgId_shouldReturnEntityWithNullOrg() { assertEquals(consumerName, result.getName()); assertEquals(idpClientId, result.getIdpClientId()); assertNull(result.getOrg()); - + // Verify verify(organisationRepository, never()).findById(any()); } @@ -148,7 +153,7 @@ void toEntity_withNullOrgId_shouldReturnEntityWithNullOrg() { void toEntity_withNonExistentOrgId_shouldReturnEntityWithNullOrg() { // Arrange when(organisationRepository.findById(orgId)).thenReturn(Optional.empty()); - + // Act Consumer result = converter.toEntity(dto); @@ -158,8 +163,8 @@ void toEntity_withNonExistentOrgId_shouldReturnEntityWithNullOrg() { 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 index fdd2335..88f230c 100644 --- 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 @@ -1,32 +1,37 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.converter.impl; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; 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.model.dto.ProductDTO; 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.entity.Product; 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; @@ -38,7 +43,7 @@ class OrganisationProducerConverterTest { 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"; @@ -49,11 +54,11 @@ class OrganisationProducerConverterTest { 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"; @@ -64,23 +69,23 @@ void setUp() { 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); @@ -93,28 +98,28 @@ void setUp() { 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); @@ -129,10 +134,10 @@ void setUp() { 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); @@ -165,25 +170,25 @@ void toDto_withValidEntity_shouldReturnCorrectDTO() { 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)); @@ -193,7 +198,7 @@ void toDto_withValidEntity_shouldReturnCorrectDTO() { void toDto_withNullOrg_shouldReturnDTOWithNullOrgId() { // Arrange entity.setOrg(null); - + // Act ProducerDTO result = converter.toDto(entity); @@ -209,12 +214,12 @@ void toDto_withNullOrg_shouldReturnDTOWithNullOrgId() { assertEquals(idpClientId, result.getIdpClientId()); assertNull(result.getOrgId()); } - + @Test void toDto_withNullProducts_shouldReturnDTOWithEmptyDataProviders() { // Arrange entity.setProducts(null); - + // Act ProducerDTO result = converter.toDto(entity); @@ -222,16 +227,16 @@ void toDto_withNullProducts_shouldReturnDTOWithEmptyDataProviders() { 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); @@ -239,7 +244,7 @@ void toDto_withEmptyProducts_shouldReturnDTOWithEmptyDataProviders() { assertNotNull(result); assertNotNull(result.getDataProviders()); assertTrue(result.getDataProviders().isEmpty()); - + // Verify productConverter was not called verify(productConverter, never()).toDto(any()); } @@ -257,7 +262,7 @@ void toEntity_withNullDTO_shouldReturnNull() { void toEntity_withValidDTO_shouldReturnCorrectEntity() { // Arrange when(organisationRepository.findById(orgId)).thenReturn(Optional.of(organisation)); - + // Act Producer result = converter.toEntity(dto); @@ -274,11 +279,11 @@ void toEntity_withValidDTO_shouldReturnCorrectEntity() { 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()); @@ -286,7 +291,7 @@ void toEntity_withValidDTO_shouldReturnCorrectEntity() { 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()); @@ -294,11 +299,11 @@ void toEntity_withValidDTO_shouldReturnCorrectEntity() { 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); } @@ -307,7 +312,7 @@ void toEntity_withValidDTO_shouldReturnCorrectEntity() { void toEntity_withNullOrgId_shouldReturnEntityWithNullOrg() { // Arrange dto.setOrgId(null); - + // Act Producer result = converter.toEntity(dto); @@ -322,7 +327,7 @@ void toEntity_withNullOrgId_shouldReturnEntityWithNullOrg() { assertEquals(tls, result.getTls()); assertEquals(idpClientId, result.getIdpClientId()); assertNull(result.getOrg()); - + // Verify verify(organisationRepository, never()).findById(any()); } @@ -331,7 +336,7 @@ void toEntity_withNullOrgId_shouldReturnEntityWithNullOrg() { void toEntity_withNonExistentOrgId_shouldReturnEntityWithNullOrg() { // Arrange when(organisationRepository.findById(orgId)).thenReturn(Optional.empty()); - + // Act Producer result = converter.toEntity(dto); @@ -346,36 +351,36 @@ void toEntity_withNonExistentOrgId_shouldReturnEntityWithNullOrg() { 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); @@ -383,22 +388,22 @@ void toEntity_withNullProducerId_shouldSetProducerIdInDataProviderDTO() { 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); @@ -406,8 +411,8 @@ void toEntity_withNullDataProviderFromConverter_shouldNotAddToProducts() { 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 index aa4ad90..61493e1 100644 --- 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 @@ -1,32 +1,37 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.converter.impl; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; 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.model.dto.ProductDTO; 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.entity.Product; 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; @@ -38,7 +43,7 @@ class ProducerConverterTest { 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"; @@ -49,11 +54,11 @@ class ProducerConverterTest { 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"; @@ -64,23 +69,23 @@ void setUp() { 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); @@ -93,28 +98,28 @@ void setUp() { 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); @@ -129,10 +134,10 @@ void setUp() { 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); @@ -165,25 +170,25 @@ void toDto_withValidEntity_shouldReturnCorrectDTO() { 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)); @@ -193,7 +198,7 @@ void toDto_withValidEntity_shouldReturnCorrectDTO() { void toDto_withNullOrg_shouldReturnDTOWithNullOrgId() { // Arrange entity.setOrg(null); - + // Act ProducerDTO result = converter.toDto(entity); @@ -209,12 +214,12 @@ void toDto_withNullOrg_shouldReturnDTOWithNullOrgId() { assertEquals(idpClientId, result.getIdpClientId()); assertNull(result.getOrgId()); } - + @Test void toDto_withNullProducts_shouldReturnDTOWithEmptyDataProviders() { // Arrange entity.setProducts(null); - + // Act ProducerDTO result = converter.toDto(entity); @@ -222,16 +227,16 @@ void toDto_withNullProducts_shouldReturnDTOWithEmptyDataProviders() { 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); @@ -239,7 +244,7 @@ void toDto_withEmptyProducts_shouldReturnDTOWithEmptyDataProviders() { assertNotNull(result); assertNotNull(result.getDataProviders()); assertTrue(result.getDataProviders().isEmpty()); - + // Verify productConverter was not called verify(productConverter, never()).toDto(any()); } @@ -257,7 +262,7 @@ void toEntity_withNullDTO_shouldReturnNull() { void toEntity_withValidDTO_shouldReturnCorrectEntity() { // Arrange when(organisationRepository.findById(orgId)).thenReturn(Optional.of(organisation)); - + // Act Producer result = converter.toEntity(dto); @@ -274,11 +279,11 @@ void toEntity_withValidDTO_shouldReturnCorrectEntity() { 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()); @@ -286,7 +291,7 @@ void toEntity_withValidDTO_shouldReturnCorrectEntity() { 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()); @@ -294,11 +299,11 @@ void toEntity_withValidDTO_shouldReturnCorrectEntity() { 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); } @@ -307,7 +312,7 @@ void toEntity_withValidDTO_shouldReturnCorrectEntity() { void toEntity_withNullOrgId_shouldReturnEntityWithNullOrg() { // Arrange dto.setOrgId(null); - + // Act Producer result = converter.toEntity(dto); @@ -322,7 +327,7 @@ void toEntity_withNullOrgId_shouldReturnEntityWithNullOrg() { assertEquals(tls, result.getTls()); assertEquals(idpClientId, result.getIdpClientId()); assertNull(result.getOrg()); - + // Verify verify(organisationRepository, never()).findById(any()); } @@ -331,7 +336,7 @@ void toEntity_withNullOrgId_shouldReturnEntityWithNullOrg() { void toEntity_withNonExistentOrgId_shouldReturnEntityWithNullOrg() { // Arrange when(organisationRepository.findById(orgId)).thenReturn(Optional.empty()); - + // Act Producer result = converter.toEntity(dto); @@ -346,36 +351,36 @@ void toEntity_withNonExistentOrgId_shouldReturnEntityWithNullOrg() { 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); @@ -383,22 +388,22 @@ void toEntity_withNullProducerId_shouldSetProducerIdInDataProviderDTO() { 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); @@ -406,8 +411,8 @@ void toEntity_withNullDataProviderFromConverter_shouldNotAddToProducts() { 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 index c15916b..521f73a 100644 --- 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 @@ -1,5 +1,16 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.converter.impl; +import static org.junit.jupiter.api.Assertions.*; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.time.Instant; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -9,12 +20,6 @@ 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 { @@ -91,4 +96,4 @@ void toEntity_withValidDTO_shouldReturnCorrectEntity() { 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 index b773793..6e1b9d6 100644 --- 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 @@ -1,5 +1,15 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.converter.impl; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -7,15 +17,10 @@ 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.entity.Product; 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 { @@ -28,7 +33,7 @@ class ProductConverterTest { 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"; @@ -41,7 +46,7 @@ void setUp() { producer = new Producer(); producer.setId(producerId); producer.setName(producerName); - + // Create test entity entity = new Product(); entity.setId(dataProviderId); @@ -83,7 +88,7 @@ void toDto_withValidEntity_shouldReturnCorrectDTO() { void toDto_withNullProducer_shouldReturnDTOWithNullProducerId() { // Arrange entity.setProducer(null); - + // Act ProductDTO result = converter.toDto(entity); @@ -108,7 +113,7 @@ void toEntity_withNullDTO_shouldReturnNull() { void toEntity_withValidDTO_shouldReturnCorrectEntity() { // Arrange when(producerRepository.findById(producerId)).thenReturn(Optional.of(producer)); - + // Act Product result = converter.toEntity(dto); @@ -120,7 +125,7 @@ void toEntity_withValidDTO_shouldReturnCorrectEntity() { assertNotNull(result.getProducer()); assertEquals(producerId, result.getProducer().getId()); assertEquals(producerName, result.getProducer().getName()); - + // Verify verify(producerRepository, times(1)).findById(producerId); } @@ -129,7 +134,7 @@ void toEntity_withValidDTO_shouldReturnCorrectEntity() { void toEntity_withNullProducerId_shouldReturnEntityWithNullProducer() { // Arrange dto.setProducerId(null); - + // Act Product result = converter.toEntity(dto); @@ -139,7 +144,7 @@ void toEntity_withNullProducerId_shouldReturnEntityWithNullProducer() { assertEquals(dataProviderName, result.getName()); assertEquals(topic, result.getTopic()); assertNull(result.getProducer()); - + // Verify verify(producerRepository, never()).findById(any()); } @@ -148,7 +153,7 @@ void toEntity_withNullProducerId_shouldReturnEntityWithNullProducer() { void toEntity_withNonExistentProducerId_shouldReturnEntityWithNullProducer() { // Arrange when(producerRepository.findById(producerId)).thenReturn(Optional.empty()); - + // Act Product result = converter.toEntity(dto); @@ -158,8 +163,8 @@ void toEntity_withNonExistentProducerId_shouldReturnEntityWithNullProducer() { 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 index fbc645e..eaf392b 100644 --- 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 @@ -1,8 +1,15 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.exception; -import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; + class AuthenticationProcessingExceptionTest { private static final String CLIENT_ID = "test-client"; @@ -13,22 +20,22 @@ class AuthenticationProcessingExceptionTest { 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 index c46f7f5..bd17993 100644 --- 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 @@ -1,8 +1,15 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.exception; -import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; + /** * Tests for the specific exception types that extend AuthenticationProcessingException. */ @@ -16,19 +23,19 @@ class SpecificExceptionsTest { 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)); @@ -36,24 +43,24 @@ void resourceAccessParsingException_withMessageCauseAndClientId_shouldIncludeCli 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)); @@ -61,24 +68,24 @@ void jwtClaimParsingException_withMessageCauseAndClientId_shouldIncludeClientIdI 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)); @@ -86,4 +93,4 @@ void tokenIntrospectionException_withMessageCauseAndClientId_shouldIncludeClient 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 index 2982b82..22e4789 100644 --- 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 @@ -1,5 +1,13 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.exception.handlers; +import static org.junit.jupiter.api.Assertions.*; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -11,8 +19,6 @@ 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 @@ -39,7 +45,8 @@ void handleAuthenticationProcessingException_shouldReturnUnauthorizedStatus() { AuthenticationProcessingException exception = new AuthenticationProcessingException(message, clientId); // Act - ResponseEntity response = exceptionHandler.handleAuthenticationProcessingException(exception, webRequest); + ResponseEntity response = + exceptionHandler.handleAuthenticationProcessingException(exception, webRequest); // Assert assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); @@ -58,7 +65,8 @@ void handleAuthenticationProcessingException_withSubclass_shouldReturnUnauthoriz JwtClaimParsingException exception = new JwtClaimParsingException(message, clientId); // Act - ResponseEntity response = exceptionHandler.handleAuthenticationProcessingException(exception, webRequest); + ResponseEntity response = + exceptionHandler.handleAuthenticationProcessingException(exception, webRequest); // Assert assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); @@ -104,4 +112,4 @@ void handleAllExceptions_shouldReturnInternalServerErrorStatus() { 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 index 08f4fe8..ded552d 100644 --- 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 @@ -1,5 +1,20 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -12,16 +27,6 @@ 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 { @@ -48,10 +53,10 @@ void setUp() { id1.setConsumerId(consumerId); id1.setProductId(101L); entity1.setId(id1); - entity1.setGrantedTs(Timestamp.from(Instant.now())); - entity1.setValidity(new BigDecimal("365")); + entity1.setGrantedTs(Timestamp.from(Instant.now())); + entity1.setValidity(new BigDecimal("365")); - entity2 = new ProductConsumer(); + entity2 = new ProductConsumer(); ProductConsumerId id2 = new ProductConsumerId(); id2.setConsumerId(consumerId); id2.setProductId(102L); @@ -91,10 +96,10 @@ void findByConsumerId_shouldReturnDTOList() { 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 index 8a03261..ad89403 100644 --- 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 @@ -1,5 +1,18 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -11,14 +24,6 @@ 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 { @@ -169,4 +174,4 @@ void getConsumersOfProviders_withEmptyProviderIds_shouldReturnEmptyMap() { // 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 index 9fd1577..99ffb52 100644 --- 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 @@ -1,5 +1,13 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; +import static org.junit.jupiter.api.Assertions.assertNotNull; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -8,8 +16,6 @@ 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 { @@ -30,4 +36,4 @@ void organisationService_shouldBeInitialized() { 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 index 609a64e..71deda0 100644 --- 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 @@ -1,5 +1,16 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Collections; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -11,12 +22,6 @@ 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 { @@ -145,4 +150,4 @@ void getProducersByClientId_withNonExistingClientId_shouldReturnEmptyList() { 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 index e3bc03e..1ec4429 100644 --- 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 @@ -1,5 +1,19 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.Collections; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -12,15 +26,6 @@ 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 { @@ -147,4 +152,4 @@ void findByDataProviderId_withNonExistingId_shouldReturnEmptyList() { 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 index 835b2bd..3917ed2 100644 --- 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 @@ -1,5 +1,16 @@ +/* + * 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. + */ + package uk.gov.dbt.ndtp.ia.node.management.service.data.impl; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Collections; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -12,12 +23,6 @@ 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 { @@ -192,4 +197,4 @@ void getProductsByProducerIds_withNullRepositoryResult_shouldReturnEmptyList() { 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 index 4c427c6..24a70a8 100644 --- 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 @@ -1,16 +1,13 @@ +/* + * 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. + */ + 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 static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; import java.math.BigDecimal; import java.sql.Timestamp; @@ -22,9 +19,17 @@ import java.util.List; import java.util.Map; import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +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.ProducerService; +import uk.gov.dbt.ndtp.ia.node.management.service.data.ProductConsumerService; +import uk.gov.dbt.ndtp.ia.node.management.service.data.ProductService; @ExtendWith(MockitoExtension.class) class ConfigurationProviderImplTest { @@ -172,10 +177,8 @@ void getConsumerConfigByClientId_withNoMatchingConsumers_shouldReturnEmptyConfig @Test void getConsumerConfigByClientId_withNoMatchingConsumerForSpecificId_shouldReturnEmptyConfig() { // Arrange - ConsumerDTO differentConsumer = ConsumerDTO.builder() - .id(999L) - .idpClientId(clientId) - .build(); + 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()); @@ -200,7 +203,7 @@ void getConsumerConfigByClientId_withNoMatchingConsumerForSpecificId_shouldRetur 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()); @@ -225,7 +228,7 @@ void getConsumerConfigByClientId_withNoValidDataProviders_shouldReturnEmptyConfi void getConsumerConfigByClientId_withExpiredValidity_shouldReturnEmptyConfig() { // Arrange List consumers = List.of(consumerDTO); - + // Create expired product consumer relationship ProductConsumerDTO expiredProductConsumer = ProductConsumerDTO.builder() .consumerId(consumerId) @@ -233,9 +236,10 @@ void getConsumerConfigByClientId_withExpiredValidity_shouldReturnEmptyConfig() { .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(consumerAllowedDataProvidersService.findByConsumerId(consumerId)) + .thenReturn(List.of(expiredProductConsumer)); when(dataProviderService.getProductsByIds(Collections.emptyList())).thenReturn(Collections.emptyList()); when(producerService.getProducersByIds(Collections.emptyList())).thenReturn(Collections.emptyList()); @@ -258,7 +262,7 @@ void getConsumerConfigByClientId_withExpiredValidity_shouldReturnEmptyConfig() { 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) @@ -266,9 +270,10 @@ void getConsumerConfigByClientId_withValidityButNoGrantedTs_shouldReturnEmptyCon .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(consumerAllowedDataProvidersService.findByConsumerId(consumerId)) + .thenReturn(List.of(invalidProductConsumer)); when(dataProviderService.getProductsByIds(Collections.emptyList())).thenReturn(Collections.emptyList()); when(producerService.getProducersByIds(Collections.emptyList())).thenReturn(Collections.emptyList()); @@ -291,19 +296,20 @@ void getConsumerConfigByClientId_withValidityButNoGrantedTs_shouldReturnEmptyCon 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(consumerAllowedDataProvidersService.findByConsumerId(consumerId)) + .thenReturn(List.of(zeroValidityProductConsumer)); when(dataProviderService.getProductsByIds(List.of(productId))).thenReturn(products); when(producerService.getProducersByIds(List.of(producerId))).thenReturn(producers); @@ -328,12 +334,10 @@ void getConsumerConfigByClientId_withNoActiveProducers_shouldReturnEmptyConfig() 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(); + ProducerDTO inactiveProducer = + ProducerDTO.builder().id(producerId).active(false).build(); when(consumerService.findByIdpClientId(clientId)).thenReturn(consumers); when(consumerAllowedDataProvidersService.findByConsumerId(consumerId)).thenReturn(productConsumers); @@ -363,13 +367,14 @@ void getProducerConfigByClientId_withValidClientIdAndNoProducerId_shouldReturnCo 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(consumerAllowedDataProvidersService.findByDataProviderId(productId)) + .thenReturn(List.of(productConsumerDTO)); when(consumerService.findById(consumerId)).thenReturn(Optional.of(consumerDTO)); // Act @@ -381,7 +386,7 @@ void getProducerConfigByClientId_withValidClientIdAndNoProducerId_shouldReturnCo 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)); @@ -395,13 +400,14 @@ void getProducerConfigByClientId_withValidClientIdAndProducerId_shouldReturnFilt 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(consumerAllowedDataProvidersService.findByDataProviderId(productId)) + .thenReturn(List.of(productConsumerDTO)); when(consumerService.findById(consumerId)).thenReturn(Optional.of(consumerDTO)); // Act @@ -496,13 +502,14 @@ void getProducerConfigByClientId_withNullConsumers_shouldInitializeConsumersList // 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(consumerAllowedDataProvidersService.findByDataProviderId(productId)) + .thenReturn(List.of(productConsumerDTO)); when(consumerService.findById(consumerId)).thenReturn(Optional.of(consumerDTO)); // Act @@ -513,7 +520,7 @@ void getProducerConfigByClientId_withNullConsumers_shouldInitializeConsumersList 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)); @@ -527,7 +534,7 @@ void getProducerConfigByClientId_withExpiredValidity_shouldNotAddConsumer() { 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) @@ -535,13 +542,14 @@ void getProducerConfigByClientId_withExpiredValidity_shouldNotAddConsumer() { .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)); + when(consumerAllowedDataProvidersService.findByDataProviderId(productId)) + .thenReturn(List.of(expiredProductConsumer)); // Act ProducerConfigDTO result = configurationProvider.getProducerConfigByClientId(clientId, Optional.empty()); @@ -550,7 +558,12 @@ void getProducerConfigByClientId_withExpiredValidity_shouldNotAddConsumer() { assertNotNull(result); assertEquals(clientId, result.getClientId()); assertEquals(1, result.getProducers().size()); - assertTrue(result.getProducers().get(0).getDataProviders().get(0).getConsumers().isEmpty()); + assertTrue(result.getProducers() + .get(0) + .getDataProviders() + .get(0) + .getConsumers() + .isEmpty()); // Verify verify(producerService).getProducersByClientId(clientId); @@ -565,7 +578,7 @@ void getProducerConfigByClientId_withValidityButNoGrantedTs_shouldNotAddConsumer 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) @@ -573,13 +586,14 @@ void getProducerConfigByClientId_withValidityButNoGrantedTs_shouldNotAddConsumer .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)); + when(consumerAllowedDataProvidersService.findByDataProviderId(productId)) + .thenReturn(List.of(invalidProductConsumer)); // Act ProducerConfigDTO result = configurationProvider.getProducerConfigByClientId(clientId, Optional.empty()); @@ -588,7 +602,12 @@ void getProducerConfigByClientId_withValidityButNoGrantedTs_shouldNotAddConsumer assertNotNull(result); assertEquals(clientId, result.getClientId()); assertEquals(1, result.getProducers().size()); - assertTrue(result.getProducers().get(0).getDataProviders().get(0).getConsumers().isEmpty()); + assertTrue(result.getProducers() + .get(0) + .getDataProviders() + .get(0) + .getConsumers() + .isEmpty()); // Verify verify(producerService).getProducersByClientId(clientId); @@ -603,13 +622,14 @@ void getProducerConfigByClientId_withConsumerNotFound_shouldNotAddConsumer() { 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(consumerAllowedDataProvidersService.findByDataProviderId(productId)) + .thenReturn(List.of(productConsumerDTO)); when(consumerService.findById(consumerId)).thenReturn(Optional.empty()); // Act @@ -619,7 +639,12 @@ void getProducerConfigByClientId_withConsumerNotFound_shouldNotAddConsumer() { assertNotNull(result); assertEquals(clientId, result.getClientId()); assertEquals(1, result.getProducers().size()); - assertTrue(result.getProducers().get(0).getDataProviders().get(0).getConsumers().isEmpty()); + assertTrue(result.getProducers() + .get(0) + .getDataProviders() + .get(0) + .getConsumers() + .isEmpty()); // Verify verify(producerService).getProducersByClientId(clientId); @@ -627,9 +652,9 @@ void getProducerConfigByClientId_withConsumerNotFound_shouldNotAddConsumer() { verify(consumerAllowedDataProvidersService).findByDataProviderId(productId); verify(consumerService).findById(consumerId); } - + // Tests for isValidProvider method through public methods - + @Test void isValidProvider_withValidityNullShouldBeValid() { // Arrange @@ -639,12 +664,13 @@ void isValidProvider_withValidityNullShouldBeValid() { .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(consumerAllowedDataProvidersService.findByConsumerId(consumerId)) + .thenReturn(List.of(validProductConsumer)); when(dataProviderService.getProductsByIds(List.of(productId))).thenReturn(products); when(producerService.getProducersByIds(List.of(producerId))).thenReturn(producers); @@ -655,7 +681,7 @@ void isValidProvider_withValidityNullShouldBeValid() { assertNotNull(result); assertEquals(1, result.getProducers().size()); } - + @Test void isValidProvider_withValidGrantedTsAndValidity_shouldBeValid() { // Arrange @@ -666,12 +692,13 @@ void isValidProvider_withValidGrantedTsAndValidity_shouldBeValid() { .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(consumerAllowedDataProvidersService.findByConsumerId(consumerId)) + .thenReturn(List.of(validProductConsumer)); when(dataProviderService.getProductsByIds(List.of(productId))).thenReturn(products); when(producerService.getProducersByIds(List.of(producerId))).thenReturn(producers); @@ -682,9 +709,9 @@ void isValidProvider_withValidGrantedTsAndValidity_shouldBeValid() { assertNotNull(result); assertEquals(1, result.getProducers().size()); } - + // Test for isValidGrantedTs method through isValidProvider - + @Test void isValidGrantedTs_withFutureDate_shouldBeValid() { // Arrange @@ -695,12 +722,13 @@ void isValidGrantedTs_withFutureDate_shouldBeValid() { .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(consumerAllowedDataProvidersService.findByConsumerId(consumerId)) + .thenReturn(List.of(validProductConsumer)); when(dataProviderService.getProductsByIds(List.of(productId))).thenReturn(products); when(producerService.getProducersByIds(List.of(producerId))).thenReturn(producers); @@ -711,4 +739,4 @@ void isValidGrantedTs_withFutureDate_shouldBeValid() { assertNotNull(result); assertEquals(1, result.getProducers().size()); } -} \ No newline at end of file +} From e1c5c811cc1cd331c3c2bf3a0a22f7d0347c4197 Mon Sep 17 00:00:00 2001 From: Nikan Negaresh <84400913+nikan-negaresh-informed@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:44:29 +0100 Subject: [PATCH 07/13] DPAV-1717 Adding Attributes to the database schema (#9) * - Add detailed OpenAPI annotations to `ConfigurationController` for producer and consumer endpoints. - Enable Swagger UI and OpenAPI generation through Springdoc configuration. - Modify `application.yml` to include placeholders for sensitive configuration and notes for local development. - Permit access to Swagger documentation endpoints in `SecurityConfig`. - Add `application-local.yml` for local environment configuration. - Update `GlobalExceptionHandler` to include exception message in error response. - Document authentication requirements and OpenAPI usage in a new `AUTHENTICATION_REQUIREMENTS.md`. - Extend `README.md` to include Swagger/OpenAPI documentation details. * DPAV-1726 Summary Add `OpenApiConfig` class to define API metadata and security scheme for Swagger/OpenAPI generation. * DPAV-1726 Summary Add `OpenApiConfig` class to define API metadata and security scheme for Swagger/OpenAPI generation. * DPAV-1726 Summary Add `OpenApiConfig` class to define API metadata and security scheme for Swagger/OpenAPI generation. * DPAV-1717 Summary Refactor data model and persistency layer to support attributes for Product-Consumer relationships. - Replace `ConsumerProviderRepository` with `ProductConsumerRepository` for consistency. - Introduce `ProductConsumerAttribute` entity, repository, and mappings. - Add `AttributesDTO` and update relevant converters (`ConsumerConverter`, `ProductConsumerConverter`). - Modify DTOs to include attributes for enhanced configurability. - Update `application.yml` to reflect required client ID changes. - Add database migration script to seed sample attributes. - General refactoring and cleanup for alignment with new attributes. --- README.md | 44 ++++++++ docs/AUTHENTICATION_REQUIREMENTS.md | 100 ++++++++++++++++++ pom.xml | 6 ++ .../node/management/config/OpenApiConfig.java | 28 +++++ .../management/config/SecurityConfig.java | 6 +- .../v1/ConfigurationController.java | 61 ++++++++++- .../converter/impl/ConsumerConverter.java | 25 ++++- .../impl/OrganisationProducerConverter.java | 7 +- .../converter/impl/ProducerConverter.java | 13 ++- .../impl/ProductConsumerConverter.java | 44 ++++++-- .../handlers/GlobalExceptionHandler.java | 11 +- .../management/model/dto/AttributesDTO.java | 21 ++++ .../management/model/dto/ConsumerDTO.java | 4 + .../management/model/dto/ProducerDTO.java | 2 +- .../model/dto/ProductConsumerDTO.java | 3 + .../node/management/model/dto/ProductDTO.java | 11 +- .../persistency/entity/ProductConsumer.java | 25 +++-- .../entity/ProductConsumerAttribute.java | 44 ++++++++ .../persistency/entity/ProductConsumerId.java | 43 -------- .../ConsumerProviderRepository.java | 25 ----- .../repository/ConsumerRepository.java | 11 +- .../repository/OrganisationRepository.java | 10 ++ .../repository/ProducerRepository.java | 25 +++++ .../ProductConsumerAttributeRepository.java | 22 ++++ .../repository/ProductConsumerRepository.java | 51 +++++++++ .../repository/ProductRepository.java | 21 ++++ .../data/impl/ProductConsumerServiceImpl.java | 14 +-- .../ConfigurationProviderImpl.java | 11 +- src/main/resources/application.yml | 22 ++-- ...82403__productConsumersAttributesTable.sql | 28 +++++ ...V20250914194456__add_sample_attributes.sql | 36 +++++++ .../converter/impl/ConsumerConverterTest.java | 62 +++++++++++ .../OrganisationProducerConverterTest.java | 20 ++-- .../converter/impl/ProducerConverterTest.java | 20 ++-- .../impl/ProductConsumerConverterTest.java | 37 +++++-- ...erProviderOrganisationServiceImplTest.java | 27 ++--- .../impl/ProductConsumerServiceImplTest.java | 34 +++--- .../ConfigurationProviderImplTest.java | 40 +++---- 38 files changed, 793 insertions(+), 221 deletions(-) create mode 100644 docs/AUTHENTICATION_REQUIREMENTS.md create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/OpenApiConfig.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/AttributesDTO.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/ProductConsumerAttribute.java delete mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/ProductConsumerId.java delete 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/ProductConsumerAttributeRepository.java create mode 100644 src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ProductConsumerRepository.java create mode 100644 src/main/resources/db/migration/V20250914182403__productConsumersAttributesTable.sql create mode 100644 src/main/resources/db/samples/V20250914194456__add_sample_attributes.sql diff --git a/README.md b/README.md index ffb713c..3f5689a 100644 --- a/README.md +++ b/README.md @@ -470,6 +470,50 @@ For production deployments, consider: - Implementing network segmentation - Regularly rotating secrets and certificates - Setting up monitoring and alerting for security events +## API Documentation + +The project includes interactive API documentation powered by Springdoc OpenAPI (OAS 3.1). This exposes both a human-friendly Swagger UI and machine-readable OpenAPI definitions. + +How to access locally (default settings): +- Swagger UI: https://localhost:8090/swagger-ui.html +- OpenAPI JSON: https://localhost:8090/v3/api-docs + + +Notes +- HTTPS: The application serves over HTTPS by default (see server.ssl in application.yml). If you use development certificates, your browser may warn about trust; proceed after trusting the dev CA as described in Certificate Setup. +- Security: The security configuration explicitly permits unauthenticated access to the documentation endpoints (/v3/api-docs/**, /swagger-ui/**, /swagger-ui.html) while keeping all other endpoints protected via OAuth2 Resource Server (JWT). See src/main/java/.../config/SecurityConfig.java for details. +- Port/environment: If you run on a different port or behind a reverse proxy, adjust the base URL accordingly. + +How Springdoc OpenAPI works in this project +- Auto-scanning: The springdoc-openapi-starter-webmvc-ui dependency scans Spring MVC controllers at startup and automatically builds an OpenAPI 3.1 specification from your request mappings, parameters, request/response bodies, and status codes. +- Annotations (optional but recommended): + - @Operation(summary = "...", description = "...") adds summaries, descriptions, and operation-level metadata. + - @Tag(name = "...") groups endpoints in the UI. + - @Parameter, @Schema, @ApiResponse add fine-grained control over params, models, and responses. +- Security schema: Because this app is an OAuth2 Resource Server (JWT), you can declare a bearerAuth security scheme to document Authorization: Bearer . Example: + + @io.swagger.v3.oas.annotations.security.SecurityScheme( + name = "bearerAuth", + type = io.swagger.v3.oas.annotations.enums.SecuritySchemeType.HTTP, + scheme = "bearer", + bearerFormat = "JWT" + ) + + Then add @SecurityRequirement(name = "bearerAuth") on secured controllers or operations. +- Global metadata: You can set title, version, and contact details using @OpenAPIDefinition on a @Configuration class if desired. + + +## Authentication Requirements + +All protected endpoints require JWT bearer tokens. Tokens must: +- Include the audience (aud) "management-node". +- Contain a `resource_access` claim with client roles used for authorization. + +Role-specific access: +- Producer Federator: must have role `access_producer_configurations` to access `/api/v1/configuration/producer`. +- Consumer Federator: must have role `access_consumer_configurations` to access `/api/v1/configuration/consumer`. + +Read the full details, examples, and Keycloak mapping guidance in [AUTHENTICATIONS](docs/AUTHENTICATION_REQUIREMENTS.md). ## Public Funding Acknowledgment This repository has been developed with public funding as part of the National Digital Twin Programme (NDTP), a UK Government initiative. NDTP, alongside its partners, has invested in this work to advance open, secure, and reusable digital twin technologies for any organisation, whether from the public or private sector, irrespective of size. diff --git a/docs/AUTHENTICATION_REQUIREMENTS.md b/docs/AUTHENTICATION_REQUIREMENTS.md new file mode 100644 index 0000000..9bef0fa --- /dev/null +++ b/docs/AUTHENTICATION_REQUIREMENTS.md @@ -0,0 +1,100 @@ +# Authentication Requirements + +**Repository:** `management-node` +**Description:** `Provides APIs to be accessed by Consumer and Producer Federators for the purpose of dynamic configuration management ` +**SPDX-License-Identifier:** `Apache-2.0 AND OGL-UK-3.0 ` + +--- +Management Node uses OAuth 2.0 with JWT bearer tokens for authentication and authorization. Tokens are typically issued by Keycloak in this project’s reference setup. + +Core requirements for every request to protected APIs: +- Bearer token: Requests must include `Authorization: Bearer `. +- Audience (aud) claim: The token MUST contain an audience that includes `"management-node"`. +- resource_access claim: The token MUST include a `resource_access` claim, which carries client-application roles used for authorization decisions. + +Notes: +- The API enforces role checks at endpoint level using Spring Security `@PreAuthorize` expressions. +- The Swagger UI documents the security scheme as HTTP bearer with JWT; you can use it to try endpoints by supplying a valid token. + +## Token structure requirements + +A compliant JWT will contain at least the following claims: +- `aud`: must include `management-node` (either as a string or within an array, depending on the issuer configuration). +- `resource_access`: an object mapping client IDs to role arrays. + +Sample JWT payload (use this structure when testing locally): +``` +{ + "exp": 1757863604, + "iat": 1757861804, + "jti": "trrtcc:a245819b-9a9f-648f-2e95-2390f6987c03", + "iss": "https://localhost:8443/realms/mng-node", + "aud": [ + "management-node", + "FEDERATOR_HEG" + ], + "sub": "ec13a601-9b02-443a-99ff-66f1eb146ae9", + "typ": "Bearer", + "azp": "FEDERATOR_BCC", + "resource_access": { + "management-node": { + "roles": [ + "access_producer_configurations", + "access_consumer_configurations", + "BrownfieldLandAvailability", + "PendingPlanningApplications" + ] + } + }, + "scope": "FEDERATOR_PRODUCER MANAGEMENT_NODE_ACCESS FEDERATOR_CONSUMER" +} +``` + +Notes: +- The aud claim may be a list (as shown) and must include "management-node". +- The resource_access.management-node.roles array must contain the role required for the API you are calling. + +## Role requirements per API + +- Producer API: Federator clients may access Producer configuration only when their token contains the role `access_producer_configurations` under the `resource_access` for the audience/client `management-node`. + - Enforcement in code: `@PreAuthorize("hasRole('ROLE_management-node:access_producer_configurations')")` on `/api/v1/configuration/producer`. + +- Consumer API: Federator clients may access Consumer configuration only when their token contains the role `access_consumer_configurations` under the `resource_access` for the audience/client `management-node`. + - Enforcement in code: `@PreAuthorize("hasRole('ROLE_management-node:access_consumer_configurations')")` on `/api/v1/configuration/consumer`. + +## How this maps to Keycloak + +- In Keycloak, roles are typically assigned to a client (here conceptually the `management-node` client) and appear in tokens under `resource_access["management-node"].roles`. +- Ensure the token’s audience includes `management-node`. This can be achieved by: + - Setting the client as an audience in the token via an Audience mapper, or + - Using the `audience resolve`/`Full Scope Allowed` as per your realm design. +- Create and assign the following client roles on the `management-node` client: + - `access_producer_configurations` + - `access_consumer_configurations` +- Assign these roles to the appropriate Producer or Consumer Federator clients or service accounts. + +## Requesting tokens (example) + +Using client credentials with mTLS (as per the project’s Keycloak setup): +``` +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=' \ + --data-urlencode 'grant_type=client_credentials' +``` + +Supply the returned access token to the Management Node API requests: +``` +curl -k 'https://localhost:8090/api/v1/configuration/producer' \ + -H 'Authorization: Bearer ' +``` + +## Summary + +- Authentication: JWT bearer tokens. +- Mandatory claims: `aud` includes `management-node`, and `resource_access` present. +- Authorization: + - Producer API requires role: `access_producer_configurations`. + - Consumer API requires role: `access_consumer_configurations`. +- Swagger/OpenAPI: Use Swagger UI at `/swagger-ui.html` to explore and test with a valid token. \ No newline at end of file diff --git a/pom.xml b/pom.xml index 91a017e..1d5ddcf 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,7 @@ 0.8.13 3.2.0 5.10.0 + 2.8.13 @@ -72,6 +73,11 @@ modelmapper ${modelmapper.version} + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc-openapi-starter-webmvc-ui.version} + org.springframework.boot spring-boot-starter-actuator diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/OpenApiConfig.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/OpenApiConfig.java new file mode 100644 index 0000000..f6d1d55 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/config/OpenApiConfig.java @@ -0,0 +1,28 @@ +/* + * 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. + */ + +package uk.gov.dbt.ndtp.ia.node.management.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Contact; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import org.springframework.context.annotation.Configuration; + +@Configuration +@OpenAPIDefinition( + info = + @Info( + title = "Management Node API", + version = "0.90.0", + description = + "APIs consumed by Consumer and Producer Federators to retrieve runtime configuration.", + contact = @Contact(name = "NDTP", email = "NDTP@businessandtrade.gov.uk"))) +@SecurityScheme(name = "bearerAuth", type = SecuritySchemeType.HTTP, scheme = "bearer", bearerFormat = "JWT") +public class OpenApiConfig { + // Configuration class to host OpenAPI metadata and security scheme +} 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 index 2ee8bd3..4f97755 100644 --- 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 @@ -10,6 +10,7 @@ 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.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter; import org.springframework.security.web.SecurityFilterChain; @@ -30,11 +31,10 @@ public SecurityConfig( @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http.csrf(csrf -> csrf.disable()) + http.csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/actuator/**") + .requestMatchers("/actuator/**", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html") .permitAll() - // .requestMatchers("/api/v1/configuration/**").permitAll() .anyRequest() .authenticated()) .oauth2ResourceServer( 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 index db99de1..44f664b 100644 --- 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 @@ -6,6 +6,14 @@ package uk.gov.dbt.ndtp.ia.node.management.controller.v1; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import java.util.Optional; import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; @@ -22,6 +30,7 @@ @RestController @RequestMapping("/api/v1/configuration") @Slf4j +@Tag(name = "Configuration", description = "Endpoints for retrieving Federators Producer and Consumer configuration.") public class ConfigurationController { private final ConfigurationProvider configurationProvider; @@ -32,19 +41,61 @@ public ConfigurationController(ConfigurationProvider configurationProvider) { @GetMapping("/producer") @PreAuthorize("hasRole('ROLE_management-node:access_producer_configurations')") + @Operation( + summary = "Get Federator Producer configuration", + description = + "Returns configuration for the authenticated client, optionally scoped to a specific producer.", + security = {@SecurityRequirement(name = "bearerAuth")}) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "Federator Producer configuration returned", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = ProducerConfigDTO.class))), + @ApiResponse(responseCode = "400", description = "Invalid request parameters"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + @ApiResponse(responseCode = "500", description = "Internal server error") + }) public ProducerConfigDTO getProducerConfigurations( - @AuthenticationPrincipal EnhancedPrincipal principal, - @RequestParam(value = "producer_id", required = false) Long producer_id) { - log.info("Preparing Producer Config for producer {}", producer_id); + @Parameter(hidden = true) @AuthenticationPrincipal EnhancedPrincipal principal, + @Parameter(name = "producer_id", description = "Optional Producer identifier to filter configuration") + @RequestParam(value = "producer_id", required = false) + Long producer_id) { + log.info("Preparing Federator Producer Config for producer {}", producer_id); return configurationProvider.getProducerConfigByClientId( principal.clientId(), producer_id != null ? Optional.of(producer_id) : Optional.empty()); } @GetMapping("/consumer") @PreAuthorize("hasRole('ROLE_management-node:access_consumer_configurations')") + @Operation( + summary = "Get Federator Consumer configuration", + description = + "Returns configuration for the authenticated client, optionally scoped to a specific consumer.", + security = {@SecurityRequirement(name = "bearerAuth")}) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "Consumer configuration returned", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = ConsumerConfigDTO.class))), + @ApiResponse(responseCode = "400", description = "Invalid request parameters"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + @ApiResponse(responseCode = "500", description = "Internal server error") + }) public ConsumerConfigDTO getConsumerConfigurations( - @AuthenticationPrincipal EnhancedPrincipal principal, - @RequestParam(value = "consumer_id", required = false) Long consumerId) { + @Parameter(hidden = true) @AuthenticationPrincipal EnhancedPrincipal principal, + @Parameter(name = "consumer_id", description = "Optional Consumer identifier to filter configuration") + @RequestParam(value = "consumer_id", required = false) + Long consumerId) { log.info("Preparing Consumer Config for client Id {} and Consumer {}", principal.clientId(), consumerId); return configurationProvider.getConsumerConfigByClientId( 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 index 254dc50..26d2f01 100644 --- 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 @@ -8,6 +8,7 @@ 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.AttributesDTO; 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.entity.Organisation; @@ -42,12 +43,34 @@ public ConsumerDTO toDto(Consumer entity) { return null; } - return ConsumerDTO.builder() + ConsumerDTO dto = ConsumerDTO.builder() .id(entity.getId()) .name(entity.getName()) .orgId(entity.getOrg() != null ? entity.getOrg().getId() : null) .idpClientId(entity.getIdpClientId()) .build(); + + // Populate attributes from associated ProductConsumers + try { + if (entity.getId() != null) { + var productConsumers = entity.getProductConsumers(); + if (productConsumers != null) { + productConsumers.stream() + .filter(pc -> pc.getProductConsumerAttributes() != null) + .flatMap(pc -> pc.getProductConsumerAttributes().stream()) + .forEach(attr -> dto.getAttributes() + .add(AttributesDTO.builder() + .name(attr.getName()) + .type(attr.getType()) + .value(attr.getValue()) + .build())); + } + } + } catch (Exception ignored) { + // Keep mapping resilient + } + + return dto; } /** 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 index 126ebb3..e0c7b30 100644 --- 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 @@ -63,8 +63,7 @@ public ProducerDTO toDto(Producer entity) { // Map dataProviders if they exist if (entity.getProducts() != null && !entity.getProducts().isEmpty()) { - entity.getProducts() - .forEach(dataProvider -> dto.getDataProviders().add(productConverter.toDto(dataProvider))); + entity.getProducts().forEach(dataProvider -> dto.getProducts().add(productConverter.toDto(dataProvider))); } return dto; @@ -100,9 +99,9 @@ public Producer toEntity(ProducerDTO dto) { } // Map dataProviders if they exist - if (dto.getDataProviders() != null && !dto.getDataProviders().isEmpty()) { + if (dto.getProducts() != null && !dto.getProducts().isEmpty()) { List dataProviders = new ArrayList<>(); - dto.getDataProviders().forEach(dataProviderDTO -> { + dto.getProducts().forEach(dataProviderDTO -> { // Set the producerId to ensure proper mapping if (dataProviderDTO.getProducerId() == null && dto.getId() != null) { dataProviderDTO.setProducerId(dto.getId()); 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 index c096839..ddff458 100644 --- 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 @@ -62,8 +62,7 @@ public ProducerDTO toDto(Producer entity) { // Map dataProviders if they exist if (entity.getProducts() != null && !entity.getProducts().isEmpty()) { - entity.getProducts() - .forEach(dataProvider -> dto.getDataProviders().add(productConverter.toDto(dataProvider))); + entity.getProducts().forEach(dataProvider -> dto.getProducts().add(productConverter.toDto(dataProvider))); } return dto; @@ -99,14 +98,14 @@ public Producer toEntity(ProducerDTO dto) { } // Map dataProviders if they exist - if (dto.getDataProviders() != null && !dto.getDataProviders().isEmpty()) { + if (dto.getProducts() != null && !dto.getProducts().isEmpty()) { List dataProviders = new ArrayList<>(); - dto.getDataProviders().forEach(dataProviderDTO -> { + dto.getProducts().forEach(product -> { // Set the producerId to ensure proper mapping - if (dataProviderDTO.getProducerId() == null && dto.getId() != null) { - dataProviderDTO.setProducerId(dto.getId()); + if (product.getProducerId() == null && dto.getId() != null) { + product.setProducerId(dto.getId()); } - Product dataProvider = productConverter.toEntity(dataProviderDTO); + Product dataProvider = productConverter.toEntity(product); if (dataProvider != null) { dataProvider.setProducer(entity); dataProviders.add(dataProvider); 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 index f39badf..e59d656 100644 --- 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 @@ -6,11 +6,15 @@ package uk.gov.dbt.ndtp.ia.node.management.converter.impl; +import java.util.List; 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.AttributesDTO; import uk.gov.dbt.ndtp.ia.node.management.model.dto.ProductConsumerDTO; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Consumer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Product; 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.entity.ProductConsumerAttribute; /** * Converter for ConsumerAllowedDataProvider entity and ConsumerAllowedDataProviderDTO. @@ -30,12 +34,26 @@ public ProductConsumerDTO toDto(ProductConsumer entity) { return null; } - return ProductConsumerDTO.builder() - .productId(entity.getId().getProductId()) - .consumerId(entity.getId().getConsumerId()) + ProductConsumerDTO dto = ProductConsumerDTO.builder() + .productId(entity.getProduct() != null ? entity.getProduct().getId() : null) + .consumerId(entity.getConsumer() != null ? entity.getConsumer().getId() : null) .grantedTs(entity.getGrantedTs()) .validity(entity.getValidity()) .build(); + + // Map attributes if available + List attrs = entity.getProductConsumerAttributes(); + if (attrs != null && !attrs.isEmpty()) { + List attributes = attrs.stream() + .map(a -> AttributesDTO.builder() + .name(a.getName()) + .type(a.getType()) + .value(a.getValue()) + .build()) + .toList(); + dto.getAttributes().addAll(attributes); + } + return dto; } /** @@ -52,15 +70,21 @@ public ProductConsumer toEntity(ProductConsumerDTO dto) { 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()); + if (dto.getProductId() != null) { + Product product = new Product(); + product.setId(dto.getProductId()); + entity.setProduct(product); + } + + if (dto.getConsumerId() != null) { + Consumer consumer = new Consumer(); + consumer.setId(dto.getConsumerId()); + entity.setConsumer(consumer); + } + return entity; } } 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 index 12d1470..607b4a7 100644 --- 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 @@ -47,9 +47,10 @@ public ResponseEntity handleAuthenticationProcessingException( String errorId = generateErrorId(); log.debug( - "Authentication processing exception occurred for client {}, error_id={}: ", + "Authentication processing exception occurred for client {}, error_id={} , path={}: ", ex.getClientId(), errorId, + request.getContextPath(), ex); ErrorResponse errorResponse = @@ -69,7 +70,7 @@ public ResponseEntity handleAuthenticationProcessingException( public ResponseEntity handleRuntimeException(RuntimeException ex, WebRequest request) { String errorId = generateErrorId(); - log.debug("Runtime exception occurred, error_id={}: ", errorId, ex); + log.debug("Runtime exception occurred, error_id={}, path={}: ", errorId, request.getContextPath(), ex); ErrorResponse errorResponse = new ErrorResponse( HttpStatus.INTERNAL_SERVER_ERROR.value(), "An internal server error occurred", errorId); @@ -88,10 +89,10 @@ public ResponseEntity handleRuntimeException(RuntimeException ex, public ResponseEntity handleAllExceptions(Exception ex, WebRequest request) { String errorId = generateErrorId(); - log.debug("Exception occurred, error_id={}: ", errorId, ex); + log.debug("Runtime exception occurred, error_id={}, path={}: ", errorId, request.getContextPath(), ex); - ErrorResponse errorResponse = - new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), "An unexpected error occurred", errorId); + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR.value(), "An unexpected error occurred: " + ex.getMessage(), errorId); return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); } diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/AttributesDTO.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/AttributesDTO.java new file mode 100644 index 0000000..77f55a1 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/model/dto/AttributesDTO.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +package uk.gov.dbt.ndtp.ia.node.management.model.dto; + +import lombok.*; + +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class AttributesDTO { + + private String name; + private String value; + private String type; +} 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 index 9f10db3..89477b6 100644 --- 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 @@ -7,6 +7,8 @@ package uk.gov.dbt.ndtp.ia.node.management.model.dto; import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.ArrayList; +import java.util.List; import lombok.*; /** @@ -27,4 +29,6 @@ public class ConsumerDTO { private Long orgId; private String idpClientId; + + private final List attributes = new ArrayList<>(); } 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 index 658dd9a..dcbdac7 100644 --- 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 @@ -21,7 +21,7 @@ @NoArgsConstructor @AllArgsConstructor public class ProducerDTO { - private final List dataProviders = new ArrayList<>(); + private final List products = new ArrayList<>(); @JsonIgnore private Long id; 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 index 03d904c..8ce94a7 100644 --- 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 @@ -8,6 +8,8 @@ import java.math.BigDecimal; import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; import lombok.*; /** @@ -23,4 +25,5 @@ public class ProductConsumerDTO { private Long consumerId; private Timestamp grantedTs; private BigDecimal validity; + private final List attributes = new ArrayList<>(); } 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 index 4c63020..cdddd9a 100644 --- 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 @@ -7,6 +7,7 @@ package uk.gov.dbt.ndtp.ia.node.management.model.dto; import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.ArrayList; import java.util.List; import lombok.*; @@ -19,14 +20,16 @@ @NoArgsConstructor @AllArgsConstructor public class ProductDTO { + @JsonIgnore private Long id; - private String name; - private String topic; - @JsonIgnore private Long producerId; - private List consumers; + private String name; + + private String topic; + + private List consumers = new ArrayList<>(); } 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 index 8799d9f..c707e8b 100644 --- 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 @@ -6,12 +6,10 @@ 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 jakarta.persistence.*; import java.math.BigDecimal; import java.sql.Timestamp; +import java.util.List; import lombok.Getter; import lombok.Setter; @@ -20,12 +18,27 @@ @Entity @Table(name = "product_consumer") public class ProductConsumer { - @EmbeddedId - private ProductConsumerId id; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; @Column(name = "granted_ts", nullable = false) private Timestamp grantedTs; @Column(name = "validity", nullable = false) private BigDecimal validity; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "product_id", nullable = false) + private Product product; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "consumer_id", nullable = false) + private Consumer consumer; + + @OneToMany(fetch = FetchType.LAZY) + @JoinColumn(name = "product_consumer_id", referencedColumnName = "id", insertable = false, updatable = false) + private List productConsumerAttributes; } diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/ProductConsumerAttribute.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/ProductConsumerAttribute.java new file mode 100644 index 0000000..75064f2 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/ProductConsumerAttribute.java @@ -0,0 +1,44 @@ +/* + * 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. + */ + +package uk.gov.dbt.ndtp.ia.node.management.persistency.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "product_consumer_attribute") +public class ProductConsumerAttribute { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Size(max = 150) + @NotNull + @Column(name = "name", nullable = false, length = 150) + private String name; + + @Size(max = 50) + @NotNull + @Column(name = "type", nullable = false, length = 50) + private String type; + + @Size(max = 500) + @NotNull + @Column(name = "value", nullable = false, length = 500) + private String value; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "product_consumer_id", nullable = false) + private ProductConsumer productConsumer; +} 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 deleted file mode 100644 index 601fd8a..0000000 --- a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/entity/ProductConsumerId.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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. - */ - -package uk.gov.dbt.ndtp.ia.node.management.persistency.entity; - -import jakarta.persistence.Column; -import jakarta.persistence.Embeddable; -import java.io.Serial; -import java.io.Serializable; -import java.util.Objects; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.Hibernate; - -@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); - } -} 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 deleted file mode 100644 index fd17a6f..0000000 --- a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ConsumerProviderRepository.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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. - */ - -package uk.gov.dbt.ndtp.ia.node.management.persistency.repository; - -import java.util.List; -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; - -@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); -} 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 index 979f9f2..c3ea1ea 100644 --- 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 @@ -17,6 +17,15 @@ 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") + /** + * Retrieves a list of {@link Consumer} entities associated with the specified provider IDs. + * The method performs a query to fetch consumers linked with products that correspond to the given provider IDs. + * + * @param providers a list of IDs of the providers whose associated consumers need to be retrieved + * @return a list of {@link Consumer} entities associated with the specified provider IDs + */ + @Query("SELECT c FROM Consumer c JOIN fetch c.productConsumers cp " + "inner join fetch cp.product p " + + "inner join fetch cp.consumer consumer " + + " WHERE p.id IN :providers") List findConsumersByProviderIds(List providers); } 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 index 208f537..4439596 100644 --- 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 @@ -10,5 +10,15 @@ import org.springframework.stereotype.Repository; import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Organisation; +/** + * Repository interface for managing {@link Organisation} entities. + * + * This interface provides CRUD operations and query methods for interacting with + * the underlying database layer as it extends the {@link JpaRepository} interface. + * It facilitates persistence and retrieval of Organisation data from the related + * database table. + * + * Primary focus is on the {@link Organisation} entity with the identifier type {@link Long}. + */ @Repository public interface OrganisationRepository extends JpaRepository {} 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 index c9dcf15..451bcc2 100644 --- 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 @@ -11,13 +11,38 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Producer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Product; +/** + * Repository interface for managing {@link Producer} entities. + * + * This interface provides methods for interacting with the underlying database, + * specifically for the {@link Producer} entity. It extends {@link JpaRepository} + * to provide standard CRUD operations and supports custom query methods. + * + * The primary focus is on enabling operations related to the {@link Producer} + * entity with the identifier type {@link Long}. + */ @Repository public interface ProducerRepository extends JpaRepository { + /** + * Retrieves a list of {@link Producer} entities, along with their associated {@link Product} entities, + * based on the provided list of producer IDs. + * + * @param ids a list of IDs of the {@link Producer} entities to be retrieved + * @return a list of {@link Producer} entities with their associated {@link Product} entities + */ @Query("SELECT o FROM Producer o JOIN FETCH o.products WHERE o.id IN :ids") List findByIds(List ids); + /** + * Retrieves a list of {@link Producer} entities, along with their associated {@link Product} entities, + * based on the provided Identity Provider (IdP) client identifier. + * + * @param idpClientId the Identity Provider client identifier used to retrieve corresponding {@link Producer} entities + * @return a list of {@link Producer} entities with their associated {@link Product} entities + */ @Query("SELECT o FROM Producer o JOIN FETCH o.products WHERE o.idpClientId IN :idpClientId") List findByIdpClientId(String idpClientId); } diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ProductConsumerAttributeRepository.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ProductConsumerAttributeRepository.java new file mode 100644 index 0000000..1955b1c --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ProductConsumerAttributeRepository.java @@ -0,0 +1,22 @@ +/* + * 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. + */ + +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.ProductConsumerAttribute; + +/** + * Repository interface for managing {@link ProductConsumerAttribute} entities. + * Provides persistence operations and query methods for interacting with the + * underlying database. + * + * Extends {@link JpaRepository} to inherit standard CRUD operations and adds + * query methods specific to {@link ProductConsumerAttribute}. + */ +@Repository +public interface ProductConsumerAttributeRepository extends JpaRepository {} diff --git a/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ProductConsumerRepository.java b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ProductConsumerRepository.java new file mode 100644 index 0000000..41f3257 --- /dev/null +++ b/src/main/java/uk/gov/dbt/ndtp/ia/node/management/persistency/repository/ProductConsumerRepository.java @@ -0,0 +1,51 @@ +/* + * 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. + */ + +package uk.gov.dbt.ndtp.ia.node.management.persistency.repository; + +import java.util.List; +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; + +/** + * Repository interface for managing and querying {@link ProductConsumer} entities in the database. + * Extends {@link JpaRepository} for basic CRUD operations and adds custom query methods for specific use cases. + */ +@Repository +public interface ProductConsumerRepository extends JpaRepository { + + /** + * Finds and retrieves a list of {@link ProductConsumer} entities associated with the specified consumer ID. + * This method employs a query that fetches details of product-consumer relationships, including associated + * consumer, product, and any attributes linked to the product-consumer relationship. + * + * @param consumerId the ID of the consumer whose associated product-consumer entities are to be queried + * @return a list of {@link ProductConsumer} entities associated with the given consumer ID + */ + @Query("Select dp from ProductConsumer dp " + "inner join fetch dp.consumer c " + + "inner join fetch dp.product p " + + "left join fetch dp.productConsumerAttributes pca " + + "where c.id=:consumerId") + List findByConsumerId(@Param("consumerId") Long consumerId); + + /** + * Finds and retrieves a list of {@link ProductConsumer} entities associated with the specified product ID. + * This method uses a query to fetch detailed information about product-consumer relationships, including + * the associated consumer, product, and any attributes linked to the product-consumer relationship. + * + * @param productId the ID of the product whose associated product-consumer entities are to be queried + * @return a list of {@link ProductConsumer} entities associated with the given product ID + */ + @Query("Select dp from ProductConsumer dp " + + "inner join fetch dp.consumer consumer " + + "inner join fetch dp.product p " + + "left join fetch dp.productConsumerAttributes pca " + + "where p.id=:productId") + List findByProductId(Long productId); +} 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 index 5d3f057..ad27315 100644 --- 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 @@ -12,12 +12,33 @@ import org.springframework.stereotype.Repository; import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Product; +/** + * Repository interface for managing {@link Product} entities. + * + * This interface extends {@link JpaRepository} to provide CRUD operations and additional + * query methods to interact with the {@link Product} database entity. It focuses on enabling + * functionality specific to retrieving products based on identifiers or associated producers. + * + * The primary focus is on the {@link Product} entity with the identifier type {@link Long}. + */ @Repository public interface ProductRepository extends JpaRepository { + /** + * Retrieves a list of {@link Product} entities based on the provided list of product IDs. + * + * @param ids a list of product IDs for which the {@link Product} entities are to be retrieved + * @return a list of {@link Product} entities matching the provided IDs + */ @Query("SELECT o FROM Product o WHERE o.id IN :ids") List findByIds(List ids); + /** + * Retrieves a list of {@link Product} entities associated with the specified producer IDs. + * + * @param producers a list of producer IDs whose associated {@link Product} entities need to be retrieved + * @return a list of {@link Product} entities linked to the specified producer IDs + */ @Query("SELECT o FROM Product o WHERE o.producer.id IN :producers") List findByProducerIds(List 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 index 60ca6d7..9eb43a3 100644 --- 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 @@ -11,7 +11,7 @@ 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.persistency.repository.ProductConsumerRepository; import uk.gov.dbt.ndtp.ia.node.management.service.data.ProductConsumerService; /** @@ -20,18 +20,18 @@ @Service public class ProductConsumerServiceImpl implements ProductConsumerService { - private final ConsumerProviderRepository consumerProviderRepository; + private final ProductConsumerRepository productConsumerRepository; private final ProductConsumerConverter consumerProviderConverter; /** * Constructor-based dependency injection. * - * @param consumerProviderRepository the consumer allowed data provider repository + * @param productConsumerRepository the consumer allowed data provider repository * @param productConsumerConverter the converter for entity-to-DTO conversion */ public ProductConsumerServiceImpl( - ConsumerProviderRepository consumerProviderRepository, ProductConsumerConverter productConsumerConverter) { - this.consumerProviderRepository = consumerProviderRepository; + ProductConsumerRepository productConsumerRepository, ProductConsumerConverter productConsumerConverter) { + this.productConsumerRepository = productConsumerRepository; this.consumerProviderConverter = productConsumerConverter; } @@ -40,13 +40,13 @@ public ProductConsumerServiceImpl( */ @Override public List findByConsumerId(Long consumerId) { - List entities = consumerProviderRepository.findByConsumerId(consumerId); + List entities = productConsumerRepository.findByConsumerId(consumerId); return consumerProviderConverter.toDtoList(entities); } @Override public List findByDataProviderId(Long providerId) { - List entities = consumerProviderRepository.findByProductId(providerId); + List entities = productConsumerRepository.findByProductId(providerId); return consumerProviderConverter.toDtoList(entities); } } 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 index e01055b..4748eaf 100644 --- 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 @@ -168,7 +168,7 @@ private List collectDataProviderIds(List producers) { for (ProducerDTO producer : producers) { List ids = - producer.getDataProviders().stream().map(ProductDTO::getId).toList(); + producer.getProducts().stream().map(ProductDTO::getId).toList(); dataProviderIds.addAll(ids); } @@ -182,7 +182,7 @@ private List collectDataProviderIds(List producers) { */ private void processConsumersForProducers(List producers) { for (ProducerDTO producer : producers) { - for (ProductDTO provider : producer.getDataProviders()) { + for (ProductDTO provider : producer.getProducts()) { processConsumersForProvider(provider); } } @@ -194,10 +194,6 @@ private void processConsumersForProducers(List producers) { * @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 = @@ -214,6 +210,9 @@ private void processConsumersForProvider(ProductDTO provider) { * @param provider the provider to add consumers to */ private void addValidConsumersToProvider(List consumerProviders, ProductDTO provider) { + if (provider.getConsumers() == null) { + provider.setConsumers(new ArrayList<>()); + } consumerProviders.stream().filter(this::isValidProvider).forEach(consumerProvider -> { Optional consumer = consumerService.findById(consumerProvider.getConsumerId()); consumer.ifPresent(provider.getConsumers()::add); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 48c3c3d..0532b6f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,17 +12,17 @@ spring: opaquetoken: introspection-uri: https://localhost:8443/realms/mng-node/protocol/openid-connect/token/introspect client-secret: - client-id: + client-id: MANAGEMENT_NODE_CLIENT # required client id for introspect endpoint flyway: create-schemas: on default-schema: mn - locations: classpath:db/migration,classpath:db/samples + locations: classpath:db/migration,classpath:db/samples # samples are only for local development enabled: true baseline-on-migrate: true datasource: url: jdbc:postgresql://localhost:5433/postgres - username: keycloak_db_user - password: keycloak_db_user_password + username: # required postgress username + password: # required postgress username jpa: properties: hibernate: @@ -35,18 +35,18 @@ server: port: 8090 ssl: key-alias: localhost - key-store: /home/developer/Downloads/managementNode/docker/keystore.jks + key-store: keystore.jks #path to ssl keystore key-store-type: JKS - key-store-password: changeit - trust-store: /home/developer/Downloads/managementNode/docker/truststore.jks - trust-store-password: changeit + key-store-password: #keystore password + trust-store: #path to ssl truststore + trust-store-password: #truststore password trust-store-type: JKS client-auth: need - enabled: true + enabled: true # disable for local development Only application: client: - key-store: /home/developer/Downloads/managementNode/docker/keystore.jks - keyStorePassword: changeit + key-store: keystore.jks # path to MTLS client keystore + keyStorePassword: # MTLS client keystore password keyStoreType: JKS # Actuator Configuration diff --git a/src/main/resources/db/migration/V20250914182403__productConsumersAttributesTable.sql b/src/main/resources/db/migration/V20250914182403__productConsumersAttributesTable.sql new file mode 100644 index 0000000..a5fec82 --- /dev/null +++ b/src/main/resources/db/migration/V20250914182403__productConsumersAttributesTable.sql @@ -0,0 +1,28 @@ +/* + * 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. + */ + + +-- Alter table to add a surrogate primary key column as requested +alter table product_consumer add column id bigserial; +-- Switch primary key from (product_id, consumer_id) to the new id column +alter table product_consumer drop constraint pk_consumer_allowed_data_provider; +alter table product_consumer add constraint pk_consumer_allowed_data_provider primary key (id); +-- Preserve uniqueness of the original natural key +alter table product_consumer add constraint uq_product_consumer_pair unique (product_id, consumer_id); + + + + +CREATE TABLE product_consumer_attribute +( + "id" bigserial NOT NULL, + name varchar(150) NOT NULL, + type varchar(50) NOT NULL, + value varchar(500) NOT NULL, + product_consumer_id bigserial NOT NULL, + CONSTRAINT PK_product_consumer_attribute_id PRIMARY KEY ( "id" ), + CONSTRAINT FK_product_consumer_attribute__product_consumer_id FOREIGN KEY ( product_consumer_id ) REFERENCES product_consumer ( "id" ) +); \ No newline at end of file diff --git a/src/main/resources/db/samples/V20250914194456__add_sample_attributes.sql b/src/main/resources/db/samples/V20250914194456__add_sample_attributes.sql new file mode 100644 index 0000000..9a931f9 --- /dev/null +++ b/src/main/resources/db/samples/V20250914194456__add_sample_attributes.sql @@ -0,0 +1,36 @@ +/* + * 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. + */ + + +insert +into product_consumer_attribute (name, type, value, product_consumer_id) +values ('nationality', 'string', 'GBR', (select id as pid + from product_consumer + where product_id = + (Select id from product where name = 'BrownfieldLandAvailability') + and consumer_id = + (Select id from consumer where idp_client_id = 'FEDERATOR_ENV'))); + +insert +into product_consumer_attribute (name, type, value, product_consumer_id) +values ('clearance', 'string', '0', (select id as pid + from product_consumer + where + product_id = (Select id from product where name = 'BrownfieldLandAvailability') + and consumer_id = + (Select id from consumer where idp_client_id = 'FEDERATOR_ENV'))); + + + +insert +into product_consumer_attribute (name, type, value, product_consumer_id) +values ('organisation_type', 'string', 'NON-GOV3', (select id as pid + from product_consumer + where product_id = + (Select id from product where name = 'BrownfieldLandAvailability') + and consumer_id = + (Select id from consumer where idp_client_id = 'FEDERATOR_ENV'))); + 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 index d47ee7b..979762e 100644 --- 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 @@ -167,4 +167,66 @@ void toEntity_withNonExistentOrgId_shouldReturnEntityWithNullOrg() { // Verify verify(organisationRepository, times(1)).findById(orgId); } + + @Test + void toDto_withAttributes_shouldPopulateAttributesFromEntity() { + // Arrange + uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumer pc1 = + new uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumer(); + uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumerAttribute a1 = + new uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumerAttribute(); + a1.setName("attr1"); + a1.setType("string"); + a1.setValue("v1"); + uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumerAttribute a2 = + new uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumerAttribute(); + a2.setName("attr2"); + a2.setType("number"); + a2.setValue("42"); + pc1.setProductConsumerAttributes(java.util.List.of(a1, a2)); + + uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumer pc2 = + new uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumer(); + uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumerAttribute a3 = + new uk.gov.dbt.ndtp.ia.node.management.persistency.entity.ProductConsumerAttribute(); + a3.setName("attr3"); + a3.setType("bool"); + a3.setValue("true"); + pc2.setProductConsumerAttributes(java.util.List.of(a3)); + + entity.setProductConsumers(java.util.List.of(pc1, pc2)); + + // Act + ConsumerDTO result = converter.toDto(entity); + + // Assert + assertNotNull(result); + assertEquals(3, result.getAttributes().size()); + assertTrue(result.getAttributes().stream() + .anyMatch(a -> a.getName().equals("attr1") + && a.getType().equals("string") + && a.getValue().equals("v1"))); + assertTrue(result.getAttributes().stream() + .anyMatch(a -> a.getName().equals("attr2") + && a.getType().equals("number") + && a.getValue().equals("42"))); + assertTrue(result.getAttributes().stream() + .anyMatch(a -> a.getName().equals("attr3") + && a.getType().equals("bool") + && a.getValue().equals("true"))); + } + + @Test + void toDto_withNoAttributes_shouldHaveEmptyAttributesList() { + // Arrange + entity.setProductConsumers(java.util.List.of()); + + // Act + ConsumerDTO result = converter.toDto(entity); + + // Assert + assertNotNull(result); + assertNotNull(result.getAttributes()); + assertEquals(0, result.getAttributes().size()); + } } 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 index 88f230c..2c11ac1 100644 --- 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 @@ -136,7 +136,7 @@ void setUp() { dto.setOrgId(orgId); // Add data provider DTOs to the producer DTO - dto.getDataProviders().addAll(dataProviderDTOs); + dto.getProducts().addAll(dataProviderDTOs); // Set up mock behavior for productConverter lenient().when(productConverter.toDto(dataProvider1)).thenReturn(dataProviderDTO1); @@ -172,18 +172,18 @@ void toDto_withValidEntity_shouldReturnCorrectDTO() { assertEquals(orgId, result.getOrgId()); // Verify dataProviders mapping - assertNotNull(result.getDataProviders()); - assertEquals(2, result.getDataProviders().size()); + assertNotNull(result.getProducts()); + assertEquals(2, result.getProducts().size()); // Verify first data provider - ProductDTO productDTO1 = result.getDataProviders().get(0); + ProductDTO productDTO1 = result.getProducts().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); + ProductDTO dataProviderDTO2 = result.getProducts().get(1); assertEquals(dataProviderId2, dataProviderDTO2.getId()); assertEquals(dataProviderName2, dataProviderDTO2.getName()); assertEquals(topic2, dataProviderDTO2.getTopic()); @@ -225,8 +225,8 @@ void toDto_withNullProducts_shouldReturnDTOWithEmptyDataProviders() { // Assert assertNotNull(result); - assertNotNull(result.getDataProviders()); - assertTrue(result.getDataProviders().isEmpty()); + assertNotNull(result.getProducts()); + assertTrue(result.getProducts().isEmpty()); // Verify productConverter was not called verify(productConverter, never()).toDto(any()); @@ -242,8 +242,8 @@ void toDto_withEmptyProducts_shouldReturnDTOWithEmptyDataProviders() { // Assert assertNotNull(result); - assertNotNull(result.getDataProviders()); - assertTrue(result.getDataProviders().isEmpty()); + assertNotNull(result.getProducts()); + assertTrue(result.getProducts().isEmpty()); // Verify productConverter was not called verify(productConverter, never()).toDto(any()); @@ -359,7 +359,7 @@ void toEntity_withNonExistentOrgId_shouldReturnEntityWithNullOrg() { @Test void toEntity_withEmptyDataProviders_shouldReturnEntityWithEmptyProducts() { // Arrange - dto.getDataProviders().clear(); + dto.getProducts().clear(); // Act Producer result = converter.toEntity(dto); 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 index 61493e1..10b42ac 100644 --- 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 @@ -136,7 +136,7 @@ void setUp() { dto.setOrgId(orgId); // Add data provider DTOs to the producer DTO - dto.getDataProviders().addAll(dataProviderDTOs); + dto.getProducts().addAll(dataProviderDTOs); // Set up mock behavior for productConverter lenient().when(productConverter.toDto(dataProvider1)).thenReturn(dataProviderDTO1); @@ -172,18 +172,18 @@ void toDto_withValidEntity_shouldReturnCorrectDTO() { assertEquals(orgId, result.getOrgId()); // Verify dataProviders mapping - assertNotNull(result.getDataProviders()); - assertEquals(2, result.getDataProviders().size()); + assertNotNull(result.getProducts()); + assertEquals(2, result.getProducts().size()); // Verify first data provider - ProductDTO productDTO1 = result.getDataProviders().get(0); + ProductDTO productDTO1 = result.getProducts().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); + ProductDTO dataProviderDTO2 = result.getProducts().get(1); assertEquals(dataProviderId2, dataProviderDTO2.getId()); assertEquals(dataProviderName2, dataProviderDTO2.getName()); assertEquals(topic2, dataProviderDTO2.getTopic()); @@ -225,8 +225,8 @@ void toDto_withNullProducts_shouldReturnDTOWithEmptyDataProviders() { // Assert assertNotNull(result); - assertNotNull(result.getDataProviders()); - assertTrue(result.getDataProviders().isEmpty()); + assertNotNull(result.getProducts()); + assertTrue(result.getProducts().isEmpty()); // Verify productConverter was not called verify(productConverter, never()).toDto(any()); @@ -242,8 +242,8 @@ void toDto_withEmptyProducts_shouldReturnDTOWithEmptyDataProviders() { // Assert assertNotNull(result); - assertNotNull(result.getDataProviders()); - assertTrue(result.getDataProviders().isEmpty()); + assertNotNull(result.getProducts()); + assertTrue(result.getProducts().isEmpty()); // Verify productConverter was not called verify(productConverter, never()).toDto(any()); @@ -359,7 +359,7 @@ void toEntity_withNonExistentOrgId_shouldReturnEntityWithNullOrg() { @Test void toEntity_withEmptyDataProviders_shouldReturnEntityWithEmptyProducts() { // Arrange - dto.getDataProviders().clear(); + dto.getProducts().clear(); // Act Producer result = converter.toEntity(dto); 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 index 521f73a..aa02628 100644 --- 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 @@ -11,14 +11,18 @@ import java.math.BigDecimal; import java.sql.Timestamp; import java.time.Instant; +import java.util.ArrayList; +import java.util.List; 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.Consumer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Product; 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.entity.ProductConsumerAttribute; @ExtendWith(MockitoExtension.class) class ProductConsumerConverterTest { @@ -37,10 +41,12 @@ class ProductConsumerConverterTest { void setUp() { // Create test entity entity = new ProductConsumer(); - ProductConsumerId id = new ProductConsumerId(); - id.setConsumerId(consumerId); - id.setProductId(dataProviderId); - entity.setId(id); + Consumer consumer = new Consumer(); + consumer.setId(consumerId); + Product product = new Product(); + product.setId(dataProviderId); + entity.setConsumer(consumer); + entity.setProduct(product); entity.setGrantedTs(grantedTs); entity.setValidity(validity); @@ -63,6 +69,15 @@ void toDto_withNullEntity_shouldReturnNull() { @Test void toDto_withValidEntity_shouldReturnCorrectDTO() { + // Add attributes to entity + ProductConsumerAttribute attr = new ProductConsumerAttribute(); + attr.setName("classification"); + attr.setType("string"); + attr.setValue("public"); + List attrs = new ArrayList<>(); + attrs.add(attr); + entity.setProductConsumerAttributes(attrs); + // Act ProductConsumerDTO result = converter.toDto(entity); @@ -72,6 +87,11 @@ void toDto_withValidEntity_shouldReturnCorrectDTO() { assertEquals(dataProviderId, result.getProductId()); assertEquals(grantedTs, result.getGrantedTs()); assertEquals(validity, result.getValidity()); + assertNotNull(result.getAttributes()); + assertEquals(1, result.getAttributes().size()); + assertEquals("classification", result.getAttributes().get(0).getName()); + assertEquals("string", result.getAttributes().get(0).getType()); + assertEquals("public", result.getAttributes().get(0).getValue()); } @Test @@ -90,9 +110,10 @@ void toEntity_withValidDTO_shouldReturnCorrectEntity() { // Assert assertNotNull(result); - assertNotNull(result.getId()); - assertEquals(consumerId, result.getId().getConsumerId()); - assertEquals(dataProviderId, result.getId().getProductId()); + assertNotNull(result.getConsumer()); + assertNotNull(result.getProduct()); + assertEquals(consumerId, result.getConsumer().getId()); + assertEquals(dataProviderId, result.getProduct().getId()); assertEquals(grantedTs, result.getGrantedTs()); assertEquals(validity, result.getValidity()); } 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 index ded552d..9f2c238 100644 --- 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 @@ -23,15 +23,16 @@ 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.Consumer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Product; 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 uk.gov.dbt.ndtp.ia.node.management.persistency.repository.ProductConsumerRepository; @ExtendWith(MockitoExtension.class) public class ConsumerProviderOrganisationServiceImplTest { @Mock - private ConsumerProviderRepository consumerProviderRepository; + private ProductConsumerRepository productConsumerRepository; @Mock private ProductConsumerConverter productConsumerConverter; @@ -49,18 +50,20 @@ public class ConsumerProviderOrganisationServiceImplTest { void setUp() { // Create test entities entity1 = new ProductConsumer(); - ProductConsumerId id1 = new ProductConsumerId(); - id1.setConsumerId(consumerId); - id1.setProductId(101L); - entity1.setId(id1); + Consumer consumer = new Consumer(); + consumer.setId(consumerId); + Product product1 = new Product(); + product1.setId(101L); + entity1.setConsumer(consumer); + entity1.setProduct(product1); 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); + Product product2 = new Product(); + product2.setId(102L); + entity2.setConsumer(consumer); + entity2.setProduct(product2); entity2.setGrantedTs(Timestamp.from(Instant.now())); entity2.setValidity(new BigDecimal("180")); @@ -83,7 +86,7 @@ void findByConsumerId_shouldReturnDTOList() { // Arrange List entities = Arrays.asList(entity1, entity2); List dtos = Arrays.asList(dto1, dto2); - when(consumerProviderRepository.findByConsumerId(consumerId)).thenReturn(entities); + when(productConsumerRepository.findByConsumerId(consumerId)).thenReturn(entities); when(productConsumerConverter.toDtoList(entities)).thenReturn(dtos); // Act 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 index 1ec4429..648b6f9 100644 --- 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 @@ -22,15 +22,16 @@ 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.Consumer; +import uk.gov.dbt.ndtp.ia.node.management.persistency.entity.Product; 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 uk.gov.dbt.ndtp.ia.node.management.persistency.repository.ProductConsumerRepository; @ExtendWith(MockitoExtension.class) class ProductConsumerServiceImplTest { @Mock - private ConsumerProviderRepository consumerProviderRepository; + private ProductConsumerRepository productConsumerRepository; @Mock private ProductConsumerConverter productConsumerConverter; @@ -46,12 +47,13 @@ class ProductConsumerServiceImplTest { @BeforeEach void setUp() { // Set up test data - ProductConsumerId id = new ProductConsumerId(); - id.setConsumerId(consumerId); - id.setProductId(productId); - productConsumer = new ProductConsumer(); - productConsumer.setId(id); + Consumer consumer = new Consumer(); + consumer.setId(consumerId); + Product product = new Product(); + product.setId(productId); + productConsumer.setConsumer(consumer); + productConsumer.setProduct(product); productConsumer.setGrantedTs(Timestamp.from(Instant.now())); productConsumer.setValidity(BigDecimal.ZERO); @@ -69,7 +71,7 @@ void findByConsumerId_withValidId_shouldReturnProductConsumerDTOs() { List productConsumers = List.of(productConsumer); List productConsumerDTOs = List.of(productConsumerDTO); - when(consumerProviderRepository.findByConsumerId(consumerId)).thenReturn(productConsumers); + when(productConsumerRepository.findByConsumerId(consumerId)).thenReturn(productConsumers); when(productConsumerConverter.toDtoList(productConsumers)).thenReturn(productConsumerDTOs); // Act @@ -82,7 +84,7 @@ void findByConsumerId_withValidId_shouldReturnProductConsumerDTOs() { assertEquals(productId, result.get(0).getProductId()); // Verify - verify(consumerProviderRepository).findByConsumerId(consumerId); + verify(productConsumerRepository).findByConsumerId(consumerId); verify(productConsumerConverter).toDtoList(productConsumers); } @@ -93,7 +95,7 @@ void findByConsumerId_withNonExistingId_shouldReturnEmptyList() { List emptyList = Collections.emptyList(); List emptyDTOList = Collections.emptyList(); - when(consumerProviderRepository.findByConsumerId(nonExistingId)).thenReturn(emptyList); + when(productConsumerRepository.findByConsumerId(nonExistingId)).thenReturn(emptyList); when(productConsumerConverter.toDtoList(emptyList)).thenReturn(emptyDTOList); // Act @@ -104,7 +106,7 @@ void findByConsumerId_withNonExistingId_shouldReturnEmptyList() { assertTrue(result.isEmpty()); // Verify - verify(consumerProviderRepository).findByConsumerId(nonExistingId); + verify(productConsumerRepository).findByConsumerId(nonExistingId); verify(productConsumerConverter).toDtoList(emptyList); } @@ -114,7 +116,7 @@ void findByDataProviderId_withValidId_shouldReturnProductConsumerDTOs() { List productConsumers = List.of(productConsumer); List productConsumerDTOs = List.of(productConsumerDTO); - when(consumerProviderRepository.findByProductId(productId)).thenReturn(productConsumers); + when(productConsumerRepository.findByProductId(productId)).thenReturn(productConsumers); when(productConsumerConverter.toDtoList(productConsumers)).thenReturn(productConsumerDTOs); // Act @@ -127,7 +129,7 @@ void findByDataProviderId_withValidId_shouldReturnProductConsumerDTOs() { assertEquals(productId, result.get(0).getProductId()); // Verify - verify(consumerProviderRepository).findByProductId(productId); + verify(productConsumerRepository).findByProductId(productId); verify(productConsumerConverter).toDtoList(productConsumers); } @@ -138,7 +140,7 @@ void findByDataProviderId_withNonExistingId_shouldReturnEmptyList() { List emptyList = Collections.emptyList(); List emptyDTOList = Collections.emptyList(); - when(consumerProviderRepository.findByProductId(nonExistingId)).thenReturn(emptyList); + when(productConsumerRepository.findByProductId(nonExistingId)).thenReturn(emptyList); when(productConsumerConverter.toDtoList(emptyList)).thenReturn(emptyDTOList); // Act @@ -149,7 +151,7 @@ void findByDataProviderId_withNonExistingId_shouldReturnEmptyList() { assertTrue(result.isEmpty()); // Verify - verify(consumerProviderRepository).findByProductId(nonExistingId); + verify(productConsumerRepository).findByProductId(nonExistingId); verify(productConsumerConverter).toDtoList(emptyList); } } 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 index 24a70a8..1a27e9c 100644 --- 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 @@ -366,7 +366,7 @@ void getProducerConfigByClientId_withValidClientIdAndNoProducerId_shouldReturnCo // Arrange List producers = List.of(producerDTO); // Add product to producer's dataProviders list - producerDTO.getDataProviders().add(productDTO); + producerDTO.getProducts().add(productDTO); Map> consumersMap = new HashMap<>(); consumersMap.put(productId.toString(), List.of(consumerDTO)); @@ -385,7 +385,7 @@ void getProducerConfigByClientId_withValidClientIdAndNoProducerId_shouldReturnCo assertEquals(clientId, result.getClientId()); assertEquals(1, result.getProducers().size()); assertEquals(producerId, result.getProducers().get(0).getId()); - assertEquals(1, result.getProducers().get(0).getDataProviders().size()); + assertEquals(1, result.getProducers().get(0).getProducts().size()); // Verify verify(producerService).getProducersByClientId(clientId); @@ -399,7 +399,7 @@ void getProducerConfigByClientId_withValidClientIdAndProducerId_shouldReturnFilt // Arrange List allProducers = List.of(producerDTO); // Add product to producer's dataProviders list - producerDTO.getDataProviders().add(productDTO); + producerDTO.getProducts().add(productDTO); Map> consumersMap = new HashMap<>(); consumersMap.put(productId.toString(), List.of(consumerDTO)); @@ -501,7 +501,7 @@ void getProducerConfigByClientId_withNullConsumers_shouldInitializeConsumersList 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); + producerDTO.getProducts().add(productDTO); Map> consumersMap = new HashMap<>(); consumersMap.put(productId.toString(), List.of(consumerDTO)); @@ -519,7 +519,7 @@ void getProducerConfigByClientId_withNullConsumers_shouldInitializeConsumersList assertNotNull(result); assertEquals(clientId, result.getClientId()); assertEquals(1, result.getProducers().size()); - assertNotNull(result.getProducers().get(0).getDataProviders().get(0).getConsumers()); + assertNotNull(result.getProducers().get(0).getProducts().get(0).getConsumers()); // Verify verify(producerService).getProducersByClientId(clientId); @@ -533,7 +533,7 @@ void getProducerConfigByClientId_withExpiredValidity_shouldNotAddConsumer() { // Arrange List producers = List.of(producerDTO); // Add product to producer's dataProviders list - producerDTO.getDataProviders().add(productDTO); + producerDTO.getProducts().add(productDTO); // Create expired product consumer relationship ProductConsumerDTO expiredProductConsumer = ProductConsumerDTO.builder() @@ -558,12 +558,8 @@ void getProducerConfigByClientId_withExpiredValidity_shouldNotAddConsumer() { assertNotNull(result); assertEquals(clientId, result.getClientId()); assertEquals(1, result.getProducers().size()); - assertTrue(result.getProducers() - .get(0) - .getDataProviders() - .get(0) - .getConsumers() - .isEmpty()); + assertTrue( + result.getProducers().get(0).getProducts().get(0).getConsumers().isEmpty()); // Verify verify(producerService).getProducersByClientId(clientId); @@ -577,7 +573,7 @@ void getProducerConfigByClientId_withValidityButNoGrantedTs_shouldNotAddConsumer // Arrange List producers = List.of(producerDTO); // Add product to producer's dataProviders list - producerDTO.getDataProviders().add(productDTO); + producerDTO.getProducts().add(productDTO); // Create product consumer relationship with validity but no grantedTs ProductConsumerDTO invalidProductConsumer = ProductConsumerDTO.builder() @@ -602,12 +598,8 @@ void getProducerConfigByClientId_withValidityButNoGrantedTs_shouldNotAddConsumer assertNotNull(result); assertEquals(clientId, result.getClientId()); assertEquals(1, result.getProducers().size()); - assertTrue(result.getProducers() - .get(0) - .getDataProviders() - .get(0) - .getConsumers() - .isEmpty()); + assertTrue( + result.getProducers().get(0).getProducts().get(0).getConsumers().isEmpty()); // Verify verify(producerService).getProducersByClientId(clientId); @@ -621,7 +613,7 @@ void getProducerConfigByClientId_withConsumerNotFound_shouldNotAddConsumer() { // Arrange List producers = List.of(producerDTO); // Add product to producer's dataProviders list - producerDTO.getDataProviders().add(productDTO); + producerDTO.getProducts().add(productDTO); Map> consumersMap = new HashMap<>(); consumersMap.put(productId.toString(), List.of(consumerDTO)); @@ -639,12 +631,8 @@ void getProducerConfigByClientId_withConsumerNotFound_shouldNotAddConsumer() { assertNotNull(result); assertEquals(clientId, result.getClientId()); assertEquals(1, result.getProducers().size()); - assertTrue(result.getProducers() - .get(0) - .getDataProviders() - .get(0) - .getConsumers() - .isEmpty()); + assertTrue( + result.getProducers().get(0).getProducts().get(0).getConsumers().isEmpty()); // Verify verify(producerService).getProducersByClientId(clientId); From 4bd93fd78ec777e7fa88e17b59b2b4704af3fb34 Mon Sep 17 00:00:00 2001 From: Nikan Negaresh <84400913+nikan-negaresh-informed@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:44:49 +0100 Subject: [PATCH 08/13] Add Maven workflow for automated builds, testing, and linting (#10) * Add Maven workflow for automated builds, testing, and linting * Add Maven wrapper configuration with Apache Maven 3.9.9 * Add step to upload JaCoCo HTML report in Maven workflow --- .github/workflows/maven.yml | 76 +++++++++++++++++++++++++++ .mvn/wrapper/maven-wrapper.properties | 19 +++++++ 2 files changed, 95 insertions(+) create mode 100644 .github/workflows/maven.yml create mode 100644 .mvn/wrapper/maven-wrapper.properties diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000..5a1c2c5 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,76 @@ +# 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. + +# 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. + +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven + +name: Automated tests + +on: + push: + branches: + - 'develop' + - 'main' + pull_request: + branches: + - 'develop' + - 'main' + workflow_call: + +permissions: + contents: read + +env: + MAVEN_CLI_OPTS: "--batch-mode --no-transfer-progress" + +jobs: + build: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v5 + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: 'temurin' + cache: maven + server-password: 'GH_PACKAGES_PAT' + - name: Build and Test + env: + GH_PACKAGES_PAT: ${{ secrets.GH_PACKAGES_PAT }} + run: ./mvnw $MAVEN_CLI_OPTS verify + - name: Upload JaCoCo HTML report + if: always() + uses: actions/upload-artifact@v4 + with: + name: jacoco-report + path: target/site/jacoco + if-no-files-found: warn + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: 'temurin' + cache: maven + server-password: 'GH_PACKAGES_PAT' + - name: Lint + env: + GH_PACKAGES_PAT: ${{ secrets.GH_PACKAGES_PAT }} + run: ./mvnw $MAVEN_CLI_OPTS spotless:check + diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..d58dfb7 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# 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. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip From ee738b8c3660fd4eb84f8fb70b7c1ad25a836e46 Mon Sep 17 00:00:00 2001 From: Nikan Negaresh <84400913+nikan-negaresh-informed@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:01:04 +0100 Subject: [PATCH 09/13] DPAV-1726 implementation of Swagger and API documentation (#8) * - Add detailed OpenAPI annotations to `ConfigurationController` for producer and consumer endpoints. - Enable Swagger UI and OpenAPI generation through Springdoc configuration. - Modify `application.yml` to include placeholders for sensitive configuration and notes for local development. - Permit access to Swagger documentation endpoints in `SecurityConfig`. - Add `application-local.yml` for local environment configuration. - Update `GlobalExceptionHandler` to include exception message in error response. - Document authentication requirements and OpenAPI usage in a new `AUTHENTICATION_REQUIREMENTS.md`. - Extend `README.md` to include Swagger/OpenAPI documentation details. * DPAV-1726 Summary Add `OpenApiConfig` class to define API metadata and security scheme for Swagger/OpenAPI generation. * DPAV-1726 Summary Add `OpenApiConfig` class to define API metadata and security scheme for Swagger/OpenAPI generation. * DPAV-1726 Summary Add `OpenApiConfig` class to define API metadata and security scheme for Swagger/OpenAPI generation. * Update `pom.xml` to clean and reformat XML structure for improved readability. No functional changes made. * Update PR template to improve formatting and readability. No functional changes made. * Add step to upload JaCoCo HTML report in Maven workflow --- ...LL_REQUEST_TEMPLATE.md => pull_request_template.md} | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) rename .github/{PULL_REQUEST_TEMPLATE.md => pull_request_template.md} (92%) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/pull_request_template.md similarity index 92% rename from .github/PULL_REQUEST_TEMPLATE.md rename to .github/pull_request_template.md index 1dab9b3..68ec382 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/pull_request_template.md @@ -1,17 +1,21 @@ ## Sensitive Credential Checks + - [ ] As the author of these changes, I have checked for any sensitive credentials prior to this review being requested. - [ ] As a reviewer of these changes, I have checked for any sensitive credentials prior to approving this merge. ## Motivation and Context + ## Description - + +- Describe your changes in detail ## How Has This Been Tested? + @@ -19,8 +23,10 @@ ## Screenshots (if appropriate): ## Checklist: + - [ ] It contains only changes required by issue (does not contain other PR) - [ ] Includes link to an issue (if apply) -- [ ] I have added tests to cover my changes. \ No newline at end of file +- [ ] I have added tests to cover my changes. + From 82ba461a9e806cc4ace0432be9e7f7ffde28393a Mon Sep 17 00:00:00 2001 From: Nikan Negaresh <84400913+nikan-negaresh-informed@users.noreply.github.com> Date: Tue, 23 Sep 2025 00:51:02 +0100 Subject: [PATCH 10/13] Enhance product filtering and API documentation (#11) * **Refactor ConfigurationProviderImpl to simplify producer retrieval** - Replaced retrieval logic for producers by removing intermediate data provider fetching and directly using `getProducersByConsumerIds`. - Updated related tests and mock configurations to reflect this change. - Simplified ProducerService, renaming and adapting methods to support new producer lookup logic via consumer IDs. * **Enhance ConfigurationProviderImpl to filter producer products by valid IDs** - Added logic to filter each producer's products based on provided validProductIds; clears products if no valid IDs exist. - Updated affected tests to validate the new filtering logic with changes to `consumerAllowedDataProvidersService` interactions. --- .../repository/ProducerRepository.java | 15 +- .../service/data/ProducerService.java | 2 +- .../data/impl/ProducerServiceImpl.java | 4 +- .../ConfigurationProviderImpl.java | 68 ++-- .../data/impl/ProducerServiceImplTest.java | 34 +- .../ConfigurationProviderImplTest.java | 296 +++--------------- 6 files changed, 86 insertions(+), 333 deletions(-) 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 index 451bcc2..a929301 100644 --- 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 @@ -15,11 +15,9 @@ /** * Repository interface for managing {@link Producer} entities. - * * This interface provides methods for interacting with the underlying database, * specifically for the {@link Producer} entity. It extends {@link JpaRepository} * to provide standard CRUD operations and supports custom query methods. - * * The primary focus is on enabling operations related to the {@link Producer} * entity with the identifier type {@link Long}. */ @@ -27,14 +25,15 @@ public interface ProducerRepository extends JpaRepository { /** - * Retrieves a list of {@link Producer} entities, along with their associated {@link Product} entities, - * based on the provided list of producer IDs. + * Retrieves a list of {@link Producer} entities, including their associated {@link Product} entities + * and linked product consumers, filtered by the provided list of consumer IDs. * - * @param ids a list of IDs of the {@link Producer} entities to be retrieved - * @return a list of {@link Producer} entities with their associated {@link Product} entities + * @param consumerIds a list of consumer IDs used to filter the {@link Producer} and associated entities + * @return a list of {@link Producer} entities along with their associated {@link Product} entities and product consumers */ - @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 p JOIN p.productConsumer pc WHERE pc.consumer.id IN :consumerIds") + List findByConsumerIds(List consumerIds); /** * Retrieves a list of {@link Producer} entities, along with their associated {@link Product} entities, 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 index 6f5de39..b0f4972 100644 --- 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 @@ -20,7 +20,7 @@ public interface ProducerService { * @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 getProducersByConsumerIds(List producerIds); List getProducersByClientId(String clientId); } 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 index 7316cae..ea33fd8 100644 --- 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 @@ -39,8 +39,8 @@ public ProducerServiceImpl( * {@inheritDoc} */ @Override - public List getProducersByIds(List producerIds) { - List producers = producerRepository.findByIds(producerIds); + public List getProducersByConsumerIds(List consumerIds) { + List producers = producerRepository.findByConsumerIds(consumerIds); // Convert entities to DTOs using the converter return organisationProducerConverter.toDtoList(producers); 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 index 4748eaf..74f71ba 100644 --- 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 @@ -17,7 +17,6 @@ import uk.gov.dbt.ndtp.ia.node.management.service.data.ConsumerService; import uk.gov.dbt.ndtp.ia.node.management.service.data.ProducerService; import uk.gov.dbt.ndtp.ia.node.management.service.data.ProductConsumerService; -import uk.gov.dbt.ndtp.ia.node.management.service.data.ProductService; @Service public class ConfigurationProviderImpl implements ConfigurationProvider { @@ -26,19 +25,15 @@ public class ConfigurationProviderImpl implements ConfigurationProvider { 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; } @@ -53,9 +48,29 @@ private static boolean isValidGrantedTs(Timestamp grantedTs, BigDecimal validity @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); + List consumerIds = consumers.stream().map(ConsumerDTO::getId).toList(); + + List validProductIds = new ArrayList<>(); + + consumers.forEach(consumer -> + validProductIds.addAll(consumerAllowedDataProvidersService.findByConsumerId(consumer.getId()).stream() + .filter(this::isValidProvider) + .map(ProductConsumerDTO::getProductId) + .toList())); + + List producers = producerService.getProducersByConsumerIds(consumerIds).stream() + .filter(ProducerDTO::getActive) + .toList(); + + // Filter products of each producer to only those in validProductIds + if (!validProductIds.isEmpty()) { + var validIdsSet = new java.util.HashSet<>(validProductIds); + producers.forEach( + p -> p.getProducts().removeIf(prod -> prod.getId() == null || !validIdsSet.contains(prod.getId()))); + } else { + // If no valid products, clear products for all producers + producers.forEach(p -> p.getProducts().clear()); + } return ConsumerConfigDTO.builder() .clientId(clientId) @@ -99,43 +114,6 @@ private List getFilteredConsumers(String clientId, Optional c 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. * 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 index 71deda0..5470f34 100644 --- 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 @@ -57,50 +57,50 @@ void setUp() { } @Test - void getProducersByIds_withValidIds_shouldReturnProducerDTOs() { + void getProducersByConsumerIds_withValidIds_shouldReturnProducerDTOs() { // Arrange - List producerIds = List.of(producerId); + List consumerIds = List.of(producerId); List producers = List.of(producer); List producerDTOs = List.of(producerDTO); - when(producerRepository.findByIds(producerIds)).thenReturn(producers); + when(producerRepository.findByConsumerIds(consumerIds)).thenReturn(producers); when(organisationProducerConverter.toDtoList(producers)).thenReturn(producerDTOs); // Act - List result = producerService.getProducersByIds(producerIds); + List result = producerService.getProducersByConsumerIds(consumerIds); // 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()); + assertEquals(producerId, result.getFirst().getId()); + assertEquals(clientId, result.getFirst().getIdpClientId()); + assertEquals("Test Producer", result.getFirst().getName()); + assertEquals(true, result.getFirst().getActive()); // Verify - verify(producerRepository).findByIds(producerIds); + verify(producerRepository).findByConsumerIds(consumerIds); verify(organisationProducerConverter).toDtoList(producers); } @Test - void getProducersByIds_withEmptyIds_shouldReturnEmptyList() { + void getProducersByConsumerIds_withEmptyIds_shouldReturnEmptyList() { // Arrange List emptyIds = Collections.emptyList(); List emptyProducers = Collections.emptyList(); List emptyDTOs = Collections.emptyList(); - when(producerRepository.findByIds(emptyIds)).thenReturn(emptyProducers); + when(producerRepository.findByConsumerIds(emptyIds)).thenReturn(emptyProducers); when(organisationProducerConverter.toDtoList(emptyProducers)).thenReturn(emptyDTOs); // Act - List result = producerService.getProducersByIds(emptyIds); + List result = producerService.getProducersByConsumerIds(emptyIds); // Assert assertNotNull(result); assertTrue(result.isEmpty()); // Verify - verify(producerRepository).findByIds(emptyIds); + verify(producerRepository).findByConsumerIds(emptyIds); verify(organisationProducerConverter).toDtoList(emptyProducers); } @@ -119,10 +119,10 @@ void getProducersByClientId_withValidClientId_shouldReturnProducerDTOs() { // 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()); + assertEquals(producerId, result.getFirst().getId()); + assertEquals(clientId, result.getFirst().getIdpClientId()); + assertEquals("Test Producer", result.getFirst().getName()); + assertEquals(true, result.getFirst().getActive()); // Verify verify(producerRepository).findByIdpClientId(clientId); 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 index 1a27e9c..1f72d7c 100644 --- 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 @@ -29,7 +29,6 @@ import uk.gov.dbt.ndtp.ia.node.management.service.data.ConsumerService; import uk.gov.dbt.ndtp.ia.node.management.service.data.ProducerService; import uk.gov.dbt.ndtp.ia.node.management.service.data.ProductConsumerService; -import uk.gov.dbt.ndtp.ia.node.management.service.data.ProductService; @ExtendWith(MockitoExtension.class) class ConfigurationProviderImplTest { @@ -40,9 +39,6 @@ class ConfigurationProviderImplTest { @Mock private ProductConsumerService consumerAllowedDataProvidersService; - @Mock - private ProductService dataProviderService; - @Mock private ProducerService producerService; @@ -98,14 +94,10 @@ void setUp() { 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); + when(producerService.getProducersByConsumerIds(List.of(consumerId))).thenReturn(producers); // Act ConsumerConfigDTO result = configurationProvider.getConsumerConfigByClientId(clientId, Optional.empty()); @@ -114,27 +106,22 @@ void getConsumerConfigByClientId_withValidClientIdAndNoConsumerId_shouldReturnCo assertNotNull(result); assertEquals(clientId, result.getClientId()); assertEquals(1, result.getProducers().size()); - assertEquals(producerId, result.getProducers().get(0).getId()); + assertEquals(producerId, result.getProducers().getFirst().getId()); // Verify verify(consumerService).findByIdpClientId(clientId); + verify(producerService).getProducersByConsumerIds(List.of(consumerId)); 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); + when(producerService.getProducersByConsumerIds(List.of(consumerId))).thenReturn(producers); // Act ConsumerConfigDTO result = configurationProvider.getConsumerConfigByClientId(clientId, Optional.of(consumerId)); @@ -143,21 +130,19 @@ void getConsumerConfigByClientId_withValidClientIdAndConsumerId_shouldReturnFilt assertNotNull(result); assertEquals(clientId, result.getClientId()); assertEquals(1, result.getProducers().size()); - assertEquals(producerId, result.getProducers().get(0).getId()); + assertEquals(producerId, result.getProducers().getFirst().getId()); // Verify verify(consumerService).findByIdpClientId(clientId); + verify(producerService).getProducersByConsumerIds(List.of(consumerId)); 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()); + when(producerService.getProducersByConsumerIds(Collections.emptyList())).thenReturn(Collections.emptyList()); // Act ConsumerConfigDTO result = configurationProvider.getConsumerConfigByClientId(clientId, Optional.empty()); @@ -169,9 +154,8 @@ void getConsumerConfigByClientId_withNoMatchingConsumers_shouldReturnEmptyConfig // Verify verify(consumerService).findByIdpClientId(clientId); - verify(consumerAllowedDataProvidersService, never()).findByConsumerId(any()); - verify(dataProviderService).getProductsByIds(Collections.emptyList()); - verify(producerService).getProducersByIds(Collections.emptyList()); + verify(producerService).getProducersByConsumerIds(Collections.emptyList()); + verifyNoInteractions(consumerAllowedDataProvidersService); } @Test @@ -181,8 +165,7 @@ void getConsumerConfigByClientId_withNoMatchingConsumerForSpecificId_shouldRetur 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()); + when(producerService.getProducersByConsumerIds(Collections.emptyList())).thenReturn(Collections.emptyList()); // Act ConsumerConfigDTO result = configurationProvider.getConsumerConfigByClientId(clientId, Optional.of(consumerId)); @@ -194,155 +177,21 @@ void getConsumerConfigByClientId_withNoMatchingConsumerForSpecificId_shouldRetur // 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)); + verify(producerService).getProducersByConsumerIds(Collections.emptyList()); + verifyNoInteractions(consumerAllowedDataProvidersService); } @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)); + when(producerService.getProducersByConsumerIds(List.of(consumerId))).thenReturn(List.of(inactiveProducer)); // Act ConsumerConfigDTO result = configurationProvider.getConsumerConfigByClientId(clientId, Optional.empty()); @@ -354,9 +203,8 @@ void getConsumerConfigByClientId_withNoActiveProducers_shouldReturnEmptyConfig() // Verify verify(consumerService).findByIdpClientId(clientId); + verify(producerService).getProducersByConsumerIds(List.of(consumerId)); verify(consumerAllowedDataProvidersService).findByConsumerId(consumerId); - verify(dataProviderService).getProductsByIds(List.of(productId)); - verify(producerService).getProducersByIds(List.of(producerId)); } // Tests for getProducerConfigByClientId @@ -384,8 +232,8 @@ void getProducerConfigByClientId_withValidClientIdAndNoProducerId_shouldReturnCo assertNotNull(result); assertEquals(clientId, result.getClientId()); assertEquals(1, result.getProducers().size()); - assertEquals(producerId, result.getProducers().get(0).getId()); - assertEquals(1, result.getProducers().get(0).getProducts().size()); + assertEquals(producerId, result.getProducers().getFirst().getId()); + assertEquals(1, result.getProducers().getFirst().getProducts().size()); // Verify verify(producerService).getProducersByClientId(clientId); @@ -417,7 +265,7 @@ void getProducerConfigByClientId_withValidClientIdAndProducerId_shouldReturnFilt assertNotNull(result); assertEquals(clientId, result.getClientId()); assertEquals(1, result.getProducers().size()); - assertEquals(producerId, result.getProducers().get(0).getId()); + assertEquals(producerId, result.getProducers().getFirst().getId()); // Verify verify(producerService).getProducersByClientId(clientId); @@ -519,7 +367,7 @@ void getProducerConfigByClientId_withNullConsumers_shouldInitializeConsumersList assertNotNull(result); assertEquals(clientId, result.getClientId()); assertEquals(1, result.getProducers().size()); - assertNotNull(result.getProducers().get(0).getProducts().get(0).getConsumers()); + assertNotNull(result.getProducers().getFirst().getProducts().getFirst().getConsumers()); // Verify verify(producerService).getProducersByClientId(clientId); @@ -558,8 +406,12 @@ void getProducerConfigByClientId_withExpiredValidity_shouldNotAddConsumer() { assertNotNull(result); assertEquals(clientId, result.getClientId()); assertEquals(1, result.getProducers().size()); - assertTrue( - result.getProducers().get(0).getProducts().get(0).getConsumers().isEmpty()); + assertTrue(result.getProducers() + .getFirst() + .getProducts() + .getFirst() + .getConsumers() + .isEmpty()); // Verify verify(producerService).getProducersByClientId(clientId); @@ -598,8 +450,12 @@ void getProducerConfigByClientId_withValidityButNoGrantedTs_shouldNotAddConsumer assertNotNull(result); assertEquals(clientId, result.getClientId()); assertEquals(1, result.getProducers().size()); - assertTrue( - result.getProducers().get(0).getProducts().get(0).getConsumers().isEmpty()); + assertTrue(result.getProducers() + .getFirst() + .getProducts() + .getFirst() + .getConsumers() + .isEmpty()); // Verify verify(producerService).getProducersByClientId(clientId); @@ -631,8 +487,12 @@ void getProducerConfigByClientId_withConsumerNotFound_shouldNotAddConsumer() { assertNotNull(result); assertEquals(clientId, result.getClientId()); assertEquals(1, result.getProducers().size()); - assertTrue( - result.getProducers().get(0).getProducts().get(0).getConsumers().isEmpty()); + assertTrue(result.getProducers() + .getFirst() + .getProducts() + .getFirst() + .getConsumers() + .isEmpty()); // Verify verify(producerService).getProducersByClientId(clientId); @@ -643,88 +503,4 @@ void getProducerConfigByClientId_withConsumerNotFound_shouldNotAddConsumer() { // 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()); - } } From 5ff4b8ca33c74cd3d97c090bd9d4299107bc1c18 Mon Sep 17 00:00:00 2001 From: Nikan Negaresh <84400913+nikan-negaresh-informed@users.noreply.github.com> Date: Mon, 29 Sep 2025 12:33:59 +0100 Subject: [PATCH 11/13] fix(NON-REQ): fixes bugs related to management node APIs authorisation (#12) * **Refactor ConfigurationProviderImpl to simplify producer retrieval** - Replaced retrieval logic for producers by removing intermediate data provider fetching and directly using `getProducersByConsumerIds`. - Updated related tests and mock configurations to reflect this change. - Simplified ProducerService, renaming and adapting methods to support new producer lookup logic via consumer IDs. * **Enhance ConfigurationProviderImpl to filter producer products by valid IDs** - Added logic to filter each producer's products based on provided validProductIds; clears products if no valid IDs exist. - Updated affected tests to validate the new filtering logic with changes to `consumerAllowedDataProvidersService` interactions. * Update authorization roles and enhance security configurations - Replaced `hasRole` with `hasAuthority` in `@PreAuthorize` annotations for consistency with Spring Security standards. - Added `@JsonProperty` for `resource_access` in `JwtToken` to ensure correct JSON mapping. - Enabled method-level security with `@EnableMethodSecurity` in `SecurityConfig`. - Updated `GlobalExceptionHandler` to handle `AccessDeniedException` and `AuthorizationDeniedException`. --- .../ndtp/ia/node/management/config/SecurityConfig.java | 2 ++ .../management/controller/v1/ConfigurationController.java | 4 ++-- .../exception/handlers/GlobalExceptionHandler.java | 8 +++++++- .../dbt/ndtp/ia/node/management/model/jwt/JwtToken.java | 4 ++++ 4 files changed, 15 insertions(+), 3 deletions(-) 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 index 4f97755..b1e9699 100644 --- 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 @@ -8,6 +8,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -17,6 +18,7 @@ @Configuration @EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true) public class SecurityConfig { private final KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter; 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 index 44f664b..323fbde 100644 --- 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 @@ -40,7 +40,7 @@ public ConfigurationController(ConfigurationProvider configurationProvider) { } @GetMapping("/producer") - @PreAuthorize("hasRole('ROLE_management-node:access_producer_configurations')") + @PreAuthorize("hasAuthority('ROLE_management-node:access_producer_configurations')") @Operation( summary = "Get Federator Producer configuration", description = @@ -71,7 +71,7 @@ public ProducerConfigDTO getProducerConfigurations( } @GetMapping("/consumer") - @PreAuthorize("hasRole('ROLE_management-node:access_consumer_configurations')") + @PreAuthorize("hasAuthority('ROLE_management-node:access_consumer_configurations')") @Operation( summary = "Get Federator Consumer configuration", description = 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 index 607b4a7..aa311c2 100644 --- 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 @@ -6,10 +6,12 @@ package uk.gov.dbt.ndtp.ia.node.management.exception.handlers; +import java.nio.file.AccessDeniedException; import java.util.UUID; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.WebRequest; @@ -41,7 +43,11 @@ private String generateErrorId() { * @param request the current request * @return a ResponseEntity with an error message */ - @ExceptionHandler(AuthenticationProcessingException.class) + @ExceptionHandler({ + AuthenticationProcessingException.class, + AccessDeniedException.class, + AuthorizationDeniedException.class + }) public ResponseEntity handleAuthenticationProcessingException( AuthenticationProcessingException ex, WebRequest request) { 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 index ef282b9..30dcdbd 100644 --- 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 @@ -7,6 +7,7 @@ package uk.gov.dbt.ndtp.ia.node.management.model.jwt; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; import java.util.Map; import lombok.AllArgsConstructor; @@ -35,7 +36,10 @@ public class JwtToken { private String typ; private String azp; private List allowedOrigins; + + @JsonProperty("resource_access") private Map resourceAccess; + private String scope; private String clientId; private String username; From e788867517961f170d9c5cdef09c7945390d48c5 Mon Sep 17 00:00:00 2001 From: Nikan Negaresh <84400913+nikan-negaresh-informed@users.noreply.github.com> Date: Wed, 1 Oct 2025 23:36:26 +0100 Subject: [PATCH 12/13] fix(DPAV-1697): fixing the identified OSS checklist requirements (#13) * Update links and references in documentation for accuracy * Add separator line in README for improved readability --- CONTRIBUTING.md | 2 +- README.md | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f8be9d4..e82223c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,7 @@ Public users and NDTP partners are encouraged to engage in the following ways: --- ## Reporting Issues If you encounter a bug, error, or inconsistency, please follow these steps: -1. Check for an existing issue under [Issues](https://github.com/National-Digital-Twin/your-repo/issues). +1. Check for an existing issue under [Issues](https://github.com/National-Digital-Twin/management-node/issues). 2. Open a new issue if no one has reported it yet. Use one of the provided issue templates. 3. Provide a clear, detailed description of the issue, including steps to reproduce it if applicable. 4. Label the issue appropriately (bug, documentation, enhancement, etc.). diff --git a/README.md b/README.md index 3f5689a..e1066c0 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ The Management Node Module is a Spring Boot application that provides APIs to be - Docker and Docker Compose - OpenSSL (for certificate generation) +--- + ## Quick Start ### Setting up Keycloak with Docker Compose @@ -513,7 +515,7 @@ Role-specific access: - Producer Federator: must have role `access_producer_configurations` to access `/api/v1/configuration/producer`. - Consumer Federator: must have role `access_consumer_configurations` to access `/api/v1/configuration/consumer`. -Read the full details, examples, and Keycloak mapping guidance in [AUTHENTICATIONS](docs/AUTHENTICATION_REQUIREMENTS.md). +Read the full details, examples, and Keycloak mapping guidance in [Authentication Requirements](docs/AUTHENTICATION_REQUIREMENTS.md). ## Public Funding Acknowledgment This repository has been developed with public funding as part of the National Digital Twin Programme (NDTP), a UK Government initiative. NDTP, alongside its partners, has invested in this work to advance open, secure, and reusable digital twin technologies for any organisation, whether from the public or private sector, irrespective of size. @@ -527,7 +529,7 @@ We take security seriously. If you believe you have found a security vulnerabili ## Software Bill of Materials (SBOM) This project provides a Software Bill of Materials (SBOM) to help users and integrators understand its dependencies. ### Current SBOM -Download the [latest SBOM for this codebase](https://github.com/[repository-name]/dependency-graph/sbom) to view the current list of components used in this repository. +Download the [latest SBOM for this codebase](https://github.com/National-digital-twin/management-node/dependency-graph/sbom) to view the current list of components used in this repository. ## Contributing We welcome contributions that align with the Programme’s objectives. Please read our `CONTRIBUTING.md` guidelines before submitting pull requests. ## Acknowledgements From ee890c243b49e93be2583e7e13af782712253aa3 Mon Sep 17 00:00:00 2001 From: Nikan Negaresh <84400913+nikan-negaresh-informed@users.noreply.github.com> Date: Wed, 1 Oct 2025 23:41:10 +0100 Subject: [PATCH 13/13] Release version 1.0.0: Update changelog, version, and add database schema documentation. (#15) --- CHANGELOG.md | 13 +-- README.md | 49 +++++++++++ docs/DATABASE_SCHEMA.md | 182 ++++++++++++++++++++++++++++++++++++++++ pom.xml | 2 +- 4 files changed, 233 insertions(+), 13 deletions(-) create mode 100644 docs/DATABASE_SCHEMA.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b87157..044731d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,20 +19,9 @@ This project follows **Semantic Versioning (SemVer)** ([semver.org](https://semv --- -## [Unreleased] -### Added -- Placeholder for upcoming features and enhancements. -### Fixed -- Placeholder for bug fixes and security updates. - -### Changed -- Placeholder for changes to existing functionality. - ---- - -## [0.90.0] - 2025-09-09 +## [1.0.0] - 2025-10-1 ### Initial release - This is the first initial changelog entry for the management-node. It introduces the baseline feature set and establishes the changelog structure following Semantic Versioning. diff --git a/README.md b/README.md index e1066c0..fb2bd14 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,65 @@ The Management Node Module is a Spring Boot application that provides APIs to be --- +## Database Schema + +For a full description of the database tables, relationships, and constraints, see the Database Schema documentation: [docs/DATABASE_SCHEMA.md](docs/DATABASE_SCHEMA.md). + +--- + ## Prerequisites - Java 21 - Maven 3.9+ - Docker and Docker Compose - OpenSSL (for certificate generation) +- Keycloak (for authentication and authorization) --- ## Quick Start +### Run the Spring Boot application + +This project is a Spring Boot application. You can run it by supplying configuration via either: +- A default application.yml (or application.yaml) file, or +- A profile-specific file application-{profile}.yml and passing the profile argument at startup. + +Quick options: + +1. Provide a default config: + - Create src/main/resources/application.yml with your local settings (SSL keystore/truststore, Keycloak client, DB, etc.). See the Configuration and Certificate Setup sections below. + - Run: + ```bash + mvn spring-boot:run + ``` + or: + ```bash + java -jar target/management-node-0.0.1.jar + ``` + +2. Use a profile-specific config: + - Create src/main/resources/application-local.yml (replace "local" with your profile name) with your settings. + - Run with the profile: + ```bash + mvn spring-boot:run -Dspring-boot.run.profiles=local + ``` + or: + ```bash + java -jar target/management-node-0.0.1.jar --spring.profiles.active=local + ``` + - You can also set the environment variable: + ```bash + export SPRING_PROFILES_ACTIVE=local + ``` + +Notes: +- Spring Boot will load application.yml and then override with application-{profile}.yml if a profile is active. +- You may also point to an external YAML using: + ```bash + java -jar target/management-node-0.0.1.jar --spring.config.location=/path/to/your.yml + ``` +- The application serves HTTPS on port 8090 by default (see server.ssl in configuration). + ### Setting up Keycloak with Docker Compose The application uses Keycloak for authentication and authorization. Follow these steps to set up Keycloak using Docker Compose: diff --git a/docs/DATABASE_SCHEMA.md b/docs/DATABASE_SCHEMA.md new file mode 100644 index 0000000..19853da --- /dev/null +++ b/docs/DATABASE_SCHEMA.md @@ -0,0 +1,182 @@ +# Database Schema + +This document describes the relational database schema used by the Management Node. The schema is applied via Flyway migrations located at: + +- `src/main/resources/db/migration/` + +The current schema is based on the following migrations: +- `V20250728142253__intial_database_tables.sql` +- `V20250914182403__productConsumersAttributesTable.sql` + +The database is designed to model Organisations, their Producers and Consumers, the Products offered by Producers, and the access grants that allow specific Consumers to access specific Products. Additional attributes can be attached to each grant. + +## Overview of Entities and Relationships + +- Organisation has many Producers and Consumers +- Producer belongs to an Organisation +- Consumer belongs to an Organisation +- Product belongs to a Producer +- Product ↔ Consumer is a many-to-many relationship implemented via the join table `product_consumer` +- Each `product_consumer` (grant) can have many `product_consumer_attribute` rows for extensible metadata + +A simple ER diagram (Mermaid): + +```mermaid +erDiagram + ORGANISATION ||--o{ PRODUCER : has + ORGANISATION ||--o{ CONSUMER : has + PRODUCER ||--o{ PRODUCT : offers + PRODUCT ||--o{ PRODUCT_CONSUMER : grants + CONSUMER ||--o{ PRODUCT_CONSUMER : consumes + PRODUCT_CONSUMER ||--o{ PRODUCT_CONSUMER_ATTRIBUTE : has + + ORGANISATION { + BIGSERIAL id PK + VARCHAR name + } + PRODUCER { + BIGSERIAL id PK + VARCHAR name + TEXT description + BIGINT org_id FK -> ORGANISATION.id + BOOLEAN active + VARCHAR host + NUMERIC port + BOOLEAN tls + VARCHAR idp_client_id + } + CONSUMER { + BIGSERIAL id PK + VARCHAR name + BIGINT org_id FK -> ORGANISATION.id + VARCHAR idp_client_id + } + PRODUCT { + BIGSERIAL id PK + VARCHAR name + VARCHAR topic + BIGINT producer_id FK -> PRODUCER.id + } + PRODUCT_CONSUMER { + BIGSERIAL id PK + BIGINT product_id FK -> PRODUCT.id + BIGINT consumer_id FK -> CONSUMER.id + TIMESTAMP granted_ts + NUMERIC validity + UNIQUE (product_id, consumer_id) + } + PRODUCT_CONSUMER_ATTRIBUTE { + BIGSERIAL id PK + VARCHAR name + VARCHAR type + VARCHAR value + BIGINT product_consumer_id FK -> PRODUCT_CONSUMER.id + } +``` + +--- + +## Tables + +### organisation +Represents an organisation that owns Producers and Consumers. + +Columns: +- `id` BIGSERIAL, primary key +- `name` VARCHAR(150), not null + +Usage: +- Parent entity for `producer` and `consumer`. + +--- + +### producer +Represents a Producer federator/service that offers one or more Products. + +Columns: +- `id` BIGSERIAL, primary key +- `name` VARCHAR(50), not null +- `description` TEXT, not null +- `org_id` BIGINT, not null, foreign key → `organisation(id)` +- `active` BOOLEAN, not null +- `host` VARCHAR(500), not null — host or base URL where the producer can be reached +- `port` NUMERIC, not null — network port (stored as numeric) +- `tls` BOOLEAN, not null — whether TLS is required for this endpoint +- `idp_client_id` VARCHAR(50), not null — identity provider client id (e.g., Keycloak). Informational; not an FK + +Usage: +- Owns `product` records. +- Links an Organisation to concrete connection details for the Producer. + +--- + +### consumer +Represents a Consumer federator/client that requests access to Products. + +Columns: +- `id` BIGSERIAL, primary key +- `name` VARCHAR(50), not null +- `org_id` BIGINT, not null, foreign key → `organisation(id)` +- `idp_client_id` VARCHAR(50), not null — identity provider client id (e.g., Keycloak). Informational; not an FK + +Usage: +- Participates in access grants via `product_consumer`. + +--- + +### product +Represents a Product (e.g., a data stream or dataset) offered by a Producer. + +Columns: +- `id` BIGSERIAL, primary key +- `name` VARCHAR(50), not null +- `topic` VARCHAR(150), not null — logical topic or channel for the product +- `producer_id` BIGINT, not null, foreign key → `producer(id)` + +Usage: +- The resource being granted to Consumers via `product_consumer`. + +--- + +### product_consumer +Join table representing an access grant that allows a Consumer to access a Product. + +Columns (after migration `V20250914182403`): +- `id` BIGSERIAL, primary key +- `product_id` BIGINT, not null, foreign key → `product(id)` +- `consumer_id` BIGINT, not null, foreign key → `consumer(id)` +- `granted_ts` TIMESTAMP, not null — timestamp when access was granted +- `validity` NUMERIC, not null — validity period/units are application-defined +- `uq_product_consumer_pair` UNIQUE (`product_id`, `consumer_id`) — ensures one grant per pair + +Notes: +- Originally used a composite primary key (`product_id`, `consumer_id`); later replaced by surrogate `id` while preserving uniqueness via `uq_product_consumer_pair`. + +Usage: +- Central record for authorization decisions: which Consumer can access which Product and since when. + +--- + +### product_consumer_attribute +Extensible attributes attached to a specific `product_consumer` grant (key/value-like rows with a simple type field). + +Columns: +- `id` BIGSERIAL, primary key +- `name` VARCHAR(150), not null — attribute name/key +- `type` VARCHAR(50), not null — attribute type (string indicator) +- `value` VARCHAR(500), not null — attribute value +- `product_consumer_id` BIGINT, not null, foreign key → `product_consumer(id)` + +Usage: +- Store additional constraints or metadata for a grant (e.g., scopes, rate limits, contractual flags). Semantics are defined by application logic. + +--- + +## Migration Notes +- Schema is versioned and applied with Flyway on application startup. +- Foreign keys enforce referential integrity among core entities. +- Consider adding database indexes on foreign key columns (`producer.producer_id`, `consumer.org_id`, `product.producer_id`, `product_consumer.product_id`, `product_consumer.consumer_id`, `product_consumer_attribute.product_consumer_id`) to optimize query performance, if not already present in future migrations. + +## Data Protection and Security +- Identity fields like `idp_client_id` are not foreign keys; they link to external IdP configuration (e.g., Keycloak) at the application layer. +- Ensure that any PII or sensitive metadata stored in attributes follows your organization’s data handling policies. \ No newline at end of file diff --git a/pom.xml b/pom.xml index 1d5ddcf..fd86a07 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ uk.gov.dbt.ndtp.ia.management.node management-node - 0.90.0 + 1.0.0 jar management-node Provides Management capabilities over IA Node Net