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": {