diff --git a/.github/workflows/test-solution.yml b/.github/workflows/test-solution.yml index 62ab6f42..7f03ed16 100644 --- a/.github/workflows/test-solution.yml +++ b/.github/workflows/test-solution.yml @@ -73,9 +73,11 @@ jobs: dotnet-sonarscanner begin /k:"DFE-Digital_flexforms-api" /o:"dfe-digital" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.coverageReportPaths=CoverageReport/SonarQube.xml dotnet build GovUK.Dfe.FlexForms.Api.sln --no-restore -p:CI=${CI} - - name: Run Tests + - name: Run Tests env: CI: true + ASPNETCORE_DATAPROTECTION_PROVIDER: NONE + DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1 run: dotnet test GovUK.Dfe.FlexForms.Api.sln --no-build --verbosity normal --collect:"XPlat Code Coverage" - name: Generate Code Coverage Report diff --git a/.gitignore b/.gitignore index d6bcfc6b..5e5a611a 100644 --- a/.gitignore +++ b/.gitignore @@ -185,4 +185,5 @@ uploads/ Directory.Build.targets.user /src/LocalPackages -.cursor \ No newline at end of file +.cursor +/encryption-keys \ No newline at end of file diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 00000000..7380cba3 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,40 @@ +# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +# This stage is used when running from VS in fast mode (Default for Debug configuration) +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +USER root +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +# This stage is used to build the service project +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +ENV CI=true +ARG BUILD_CONFIGURATION=Debug +WORKDIR /src +COPY ["Directory.Build.props", "."] +COPY ["src/GovUK.Dfe.FlexForms.Api/GovUK.Dfe.FlexForms.Api.csproj", "src/GovUK.Dfe.FlexForms.Api/"] +COPY ["src/GovUK.Dfe.FlexForms.Application/GovUK.Dfe.FlexForms.Application.csproj", "src/GovUK.Dfe.FlexForms.Application/"] +COPY ["src/GovUK.Dfe.FlexForms.Domain/GovUK.Dfe.FlexForms.Domain.csproj", "src/GovUK.Dfe.FlexForms.Domain/"] +COPY ["src/GovUK.Dfe.FlexForms.Utils/GovUK.Dfe.FlexForms.Utils.csproj", "src/GovUK.Dfe.FlexForms.Utils/"] +COPY ["src/GovUK.Dfe.FlexForms.Infrastructure/GovUK.Dfe.FlexForms.Infrastructure.csproj", "src/GovUK.Dfe.FlexForms.Infrastructure/"] +RUN dotnet restore "./src/GovUK.Dfe.FlexForms.Api/GovUK.Dfe.FlexForms.Api.csproj" +COPY . . +WORKDIR "/src/src/GovUK.Dfe.FlexForms.Api" +RUN dotnet build "./GovUK.Dfe.FlexForms.Api.csproj" -c $BUILD_CONFIGURATION -p:SkipDockerCompose=true -o /app/build + +# This stage is used to publish the service project to be copied to the final stage +FROM build AS publish +ARG BUILD_CONFIGURATION=Debug +RUN dotnet publish "./GovUK.Dfe.FlexForms.Api.csproj" -c $BUILD_CONFIGURATION -p:SkipDockerCompose=true -o /app/publish /p:UseAppHost=false + +# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration) +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . + +COPY script/dev-api-docker-entrypoint.sh /app/docker-entrypoint.sh +RUN sed -i 's/\r$//' /app/docker-entrypoint.sh +RUN chmod +x /app/docker-entrypoint.sh + +ENTRYPOINT ["/app/docker-entrypoint.sh"] \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..7cdf4d21 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,47 @@ +name: dfe-flexforms + +networks: + dfe-flexforms-network: + name: dfe-flexforms-network + driver: bridge + external: true + dfe-shared-network: + name: dfe-shared-network + external: true + +services: + ea_api: + container_name: ea-flexforms-api + cap_add: + - SYS_PTRACE + security_opt: + - seccomp:unconfined + build: + context: ${REPO_PATH} + dockerfile: Dockerfile.dev + ports: + - 5277:8080 + - 7089:8081 + restart: unless-stopped + environment: + - ConnectionStrings__DefaultConnection=Server=sql,1433;Database=ExternalApplications;User Id=SA;Password=Pa55w0rd!;TrustServerCertificate=True; + - ConnectionStrings__TenantConfigDatabase=Server=sql,1433;Database=TenantConfig;User Id=SA;Password=Pa55w0rd!;TrustServerCertificate=True; + - ConnectionStrings__Redis=redis:6379 + - CacheSettings__Redis__ConnectionString=redis:6379 + - DataProtection__UseAzure=false + - DataProtection__UseStorageSas=false + - DataProtection__ApplicationName=GovUK.Dfe.FlexForms.Api + - DataProtection__LocalKeysPath=/home/app/.aspnet/DataProtection-Keys + - Tenants__CodeGeneration__ConnectionStrings__Redis=redis:6379 + - ASPNETCORE_URLS=https://+:8081;http://+:8080 + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_Kestrel__Certificates__Default__Password=Pa55w0rd + - ASPNETCORE_Kestrel__Certificates__Default__Path=/https/docker_dev_cert.pfx + volumes: + - ${APPDATA}/dev-certs/docker_dev_cert.pfx:/https/docker_dev_cert.pfx:ro + - ${APPDATA}/Microsoft/UserSecrets/f714ca7a-fc08-46ff-b0cc-373d6f04cf4c:/home/app/.microsoft/usersecrets/f714ca7a-fc08-46ff-b0cc-373d6f04cf4c:ro + - ${APPDATA}/mkcert/rootCA.pem:/usr/local/share/ca-certificates/mkcert-rootCA.crt:ro + - ${APPDATA}/DataProtection-Keys:/home/app/.aspnet/DataProtection-Keys:rw + networks: + - dfe-flexforms-network + - dfe-shared-network \ No newline at end of file diff --git a/script/dev-api-docker-entrypoint.sh b/script/dev-api-docker-entrypoint.sh new file mode 100644 index 00000000..f389c8fc --- /dev/null +++ b/script/dev-api-docker-entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -e + +#apt-get update +#update-ca-certificates + +exec su app -s /bin/sh -c "dotnet GovUK.Dfe.FlexForms.Api.dll" \ No newline at end of file diff --git a/src/GovUK.Dfe.FlexForms.Api.Client/Directory.Build.targets b/src/GovUK.Dfe.FlexForms.Api.Client/Directory.Build.targets new file mode 100644 index 00000000..86048979 --- /dev/null +++ b/src/GovUK.Dfe.FlexForms.Api.Client/Directory.Build.targets @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/src/GovUK.Dfe.FlexForms.Api/Directory.Build.targets b/src/GovUK.Dfe.FlexForms.Api/Directory.Build.targets new file mode 100644 index 00000000..86048979 --- /dev/null +++ b/src/GovUK.Dfe.FlexForms.Api/Directory.Build.targets @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/src/GovUK.Dfe.FlexForms.Api/Security/DataProtectionSettings.cs b/src/GovUK.Dfe.FlexForms.Api/Security/DataProtectionSettings.cs index 89d54d25..dc27e22b 100644 --- a/src/GovUK.Dfe.FlexForms.Api/Security/DataProtectionSettings.cs +++ b/src/GovUK.Dfe.FlexForms.Api/Security/DataProtectionSettings.cs @@ -42,4 +42,14 @@ public sealed class DataProtectionSettings /// Always accessed with managed identity / DefaultAzureCredential. /// public string? KeyVaultKeyId { get; set; } + + /// + /// Directory for the local file-system key ring (for example + /// /home/app/.aspnet/DataProtection-Keys in the API container). + /// Bind-mount the host key directory to this path (read-only is fine). + /// When the path is not writable, XML keys are copied to a temp directory + /// and automatic key generation is disabled so the container only decrypts. + /// Leave empty to use the ASP.NET default key location. + /// + public string LocalKeysPath { get; set; } = "/home/app/.aspnet/DataProtection-Keys"; } diff --git a/src/GovUK.Dfe.FlexForms.Api/Security/TenantSettingsDataProtectionExtensions.cs b/src/GovUK.Dfe.FlexForms.Api/Security/TenantSettingsDataProtectionExtensions.cs index 029d71bc..6f937b55 100644 --- a/src/GovUK.Dfe.FlexForms.Api/Security/TenantSettingsDataProtectionExtensions.cs +++ b/src/GovUK.Dfe.FlexForms.Api/Security/TenantSettingsDataProtectionExtensions.cs @@ -27,10 +27,18 @@ public static IDataProtectionBuilder AddTenantSettingsDataProtection( // Local/Development without Azure opt-in: default key ring only (no SetApplicationName) // so existing locally encrypted TenantSettings remain decryptable. - var builder = services.AddDataProtection(); - + IDataProtectionBuilder builder; if (ShouldUseLocalKeyRing(environment, settings)) + { + Console.WriteLine("Using local key ring for Data Protection (no Azure)."); + Console.WriteLine(settings.LocalKeysPath); + builder = services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo(settings.LocalKeysPath)).SetApplicationName(settings.ApplicationName); return builder; + } + else + { + builder = services.AddDataProtection(); + } var applicationName = string.IsNullOrWhiteSpace(settings.ApplicationName) ? "GovUK.Dfe.FlexForms.Api" diff --git a/src/GovUK.Dfe.FlexForms.Api/appsettings.CodeGeneration.json b/src/GovUK.Dfe.FlexForms.Api/appsettings.CodeGeneration.json index e1df5381..cbc78fd4 100644 --- a/src/GovUK.Dfe.FlexForms.Api/appsettings.CodeGeneration.json +++ b/src/GovUK.Dfe.FlexForms.Api/appsettings.CodeGeneration.json @@ -7,7 +7,8 @@ "UseStorageSas": false, "ApplicationName": "GovUK.Dfe.FlexForms.Api", "BlobUri": "", - "KeyVaultKeyId": "" + "KeyVaultKeyId": "", + "LocalKeysPath": "/home/app/.aspnet/DataProtection-Keys" }, "GlobalConfiguration": { "FileStorage": { diff --git a/src/GovUK.Dfe.FlexForms.Api/appsettings.json b/src/GovUK.Dfe.FlexForms.Api/appsettings.json index 3827dc14..d4684636 100644 --- a/src/GovUK.Dfe.FlexForms.Api/appsettings.json +++ b/src/GovUK.Dfe.FlexForms.Api/appsettings.json @@ -59,7 +59,8 @@ "UseStorageSas": false, "ApplicationName": "GovUK.Dfe.FlexForms.Api", "BlobUri": "", - "KeyVaultKeyId": "" + "KeyVaultKeyId": "", + "LocalKeysPath": "/home/app/.aspnet/DataProtection-Keys" }, "GlobalConfiguration": { "ApplicationInsights": {