diff --git a/.azure/applications/dashboard/main.bicep b/.azure/applications/dashboard/main.bicep new file mode 100644 index 000000000..a83401447 --- /dev/null +++ b/.azure/applications/dashboard/main.bicep @@ -0,0 +1,127 @@ +param location string +param containerImage string +param azureNamePrefix string +param keyVaultName string +param appRegistrationId string +param appRegistrationClientSecret string +param tenantId string +param allowedGroupId string + +resource keyVault 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = { + name: keyVaultName +} + +resource keyVaultAccessPolicy 'Microsoft.KeyVault/vaults/accessPolicies@2022-07-01' = { + name: '${keyVaultName}/add' + properties: { + accessPolicies: [ + { + objectId: containerApp.identity.principalId + tenantId: subscription().tenantId + permissions: { + secrets: [ + 'get' + 'list' + ] + } + } + ] + } +} + +resource containerApp 'Microsoft.App/containerApps@2023-05-01' = { + name: '${azureNamePrefix}-dashboard' + location: location + identity: { + type: 'SystemAssigned' + } + properties: { + environmentId: resourceId('Microsoft.App/managedEnvironments', '${azureNamePrefix}-env') + configuration: { + ingress: { + external: true + targetPort: 2526 + allowInsecure: false + } + secrets: [ + { + name: 'correspondence-migration-connection-string' + keyVaultUrl: '${keyVault.properties.vaultUri}secrets/correspondence-migration-connection-string' + identity: 'System' + } + { + name: 'app-registration-client-secret' + value: appRegistrationClientSecret + } + ] + } + template: { + containers: [ + { + name: 'dashboard' + image: containerImage + env: [ + { + name: 'DatabaseOptions__ConnectionString' + secretRef: 'correspondence-migration-connection-string' + } + ] + } + ] + scale: { + minReplicas: 1 + maxReplicas: 1 + } + } + } +} + +// Configure authentication for Container App +resource containerAppAuth 'Microsoft.App/containerApps/authConfigs@2023-05-01' = { + name: 'current' + parent: containerApp + properties: { + platform: { + enabled: true + } + identityProviders: { + azureActiveDirectory: { + enabled: true + login: { + disableWWWAuthenticate: false + } + registration: { + clientId: appRegistrationId + clientSecretSettingName: 'app-registration-client-secret' + openIdIssuer: 'https://sts.windows.net/${tenantId}/' + } + validation: { + allowedAudiences: [ + 'api://${appRegistrationId}' + ] + defaultAuthorizationPolicy: { + allowedPrincipals: { + groups: [ + allowedGroupId + ] + identities: [ + allowedGroupId + ] + } + allowedApplications: [ + '${appRegistrationId}' + ] + } + jwtClaimChecks: { + allowedClientApplications: [ + '${appRegistrationId}' + ] + allowedGroups: [ + allowedGroupId + ] + } + } + } + } + } +} diff --git a/.azure/applications/dashboard/main.bicepparam b/.azure/applications/dashboard/main.bicepparam new file mode 100644 index 000000000..dc3310203 --- /dev/null +++ b/.azure/applications/dashboard/main.bicepparam @@ -0,0 +1,10 @@ +using './main.bicep' + +param azureNamePrefix = readEnvironmentVariable('AZURE_NAME_PREFIX') +param location = 'Norway East' +param containerImage = readEnvironmentVariable('containerImage') +param keyVaultName = readEnvironmentVariable('AZURE_ENVIRONMENT_KEY_VAULT_NAME') +param appRegistrationId = readEnvironmentVariable('AZURE_APP_REGISTRATION_ID') +param tenantId = readEnvironmentVariable('AZURE_TENANT_ID') +param allowedGroupId = readEnvironmentVariable('DASHBOARD_ALLOWED_GROUP_ID') +param appRegistrationClientSecret = readEnvironmentVariable('AZURE_APP_REGISTRATION_CLIENT_SECRET') diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..fe1152bdb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/.github/workflows/deploy-dashboard.yml b/.github/workflows/deploy-dashboard.yml new file mode 100644 index 000000000..9da299607 --- /dev/null +++ b/.github/workflows/deploy-dashboard.yml @@ -0,0 +1,69 @@ +name: Deploy Dashboard + +on: + workflow_dispatch: + inputs: + environment: + type: choice + description: "Environment to deploy to" + options: + - test + - staging + - production + - yt01 + - at21 + - at22 + - at23 + - at24 + +permissions: + id-token: write + contents: read + packages: write + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + environment: ${{ github.event.inputs.environment }} + + steps: + - uses: actions/checkout@v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: tools/Altinn.Correspondence.Dashboard/Dockerfile + push: true + tags: | + ghcr.io/altinn/altinn-correspondence-dashboard:${{ github.sha }} + ghcr.io/altinn/altinn-correspondence-dashboard:latest + + - name: Azure Login + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Deploy Bicep + uses: azure/arm-deploy@v1 + with: + scope: resourcegroup + resourceGroupName: ${{ secrets.RESOURCE_GROUP }} + template: .azure/applications/dashboard/main.bicep + parameters: > + AZURE_NAME_PREFIX: ${{ secrets.AZURE_NAME_PREFIX }} + containerImage=ghcr.io/altinn/altinn-correspondence-dashboard:${{ github.sha }} + AZURE_ENVIRONMENT_KEY_VAULT_NAME: ${{ secrets.AZURE_ENVIRONMENT_KEY_VAULT_NAME }} + AZURE_APP_REGISTRATION_ID: ${{ secrets.AZURE_APP_REGISTRATION_ID }} + AZURE_APP_REGISTRATION_CLIENT_SECRET: ${{ secrets.AZURE_APP_REGISTRATION_CLIENT_SECRET }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + DASHBOARD_ALLOWED_GROUP_ID: ${{ secrets.AZURE_TEST_ACCESS_CLIENT_ID }} diff --git a/Altinn.Correspondence.sln b/Altinn.Correspondence.sln index d9e3ca576..f8b7614cf 100644 --- a/Altinn.Correspondence.sln +++ b/Altinn.Correspondence.sln @@ -21,6 +21,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Altinn.Correspondence.Commo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Altinn.Correspondence.LoadTests.DatabasePopulater", "Test\Altinn.Correspondence.LoadTests.DatabasePopulater\Altinn.Correspondence.LoadTests.DatabasePopulater.csproj", "{3E5EEBF3-366E-48F8-9D57-1975F95D4FAD}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{C20EA94C-2635-4683-8EF4-E98C6EBF811F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Altinn.Correspondence.Dashboard", "tools\Altinn.Correspondence.Dashboard\Altinn.Correspondence.Dashboard.csproj", "{11558171-1B06-40A8-A230-3E136B9F187A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -59,6 +63,10 @@ Global {3E5EEBF3-366E-48F8-9D57-1975F95D4FAD}.Debug|Any CPU.Build.0 = Debug|Any CPU {3E5EEBF3-366E-48F8-9D57-1975F95D4FAD}.Release|Any CPU.ActiveCfg = Release|Any CPU {3E5EEBF3-366E-48F8-9D57-1975F95D4FAD}.Release|Any CPU.Build.0 = Release|Any CPU + {11558171-1B06-40A8-A230-3E136B9F187A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11558171-1B06-40A8-A230-3E136B9F187A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11558171-1B06-40A8-A230-3E136B9F187A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11558171-1B06-40A8-A230-3E136B9F187A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -66,6 +74,7 @@ Global GlobalSection(NestedProjects) = preSolution {D78A36B1-D457-4FF7-B01E-82A0C3C64EDC} = {DC791FEF-108F-4773-8B84-56986CB90767} {3E5EEBF3-366E-48F8-9D57-1975F95D4FAD} = {DC791FEF-108F-4773-8B84-56986CB90767} + {11558171-1B06-40A8-A230-3E136B9F187A} = {C20EA94C-2635-4683-8EF4-E98C6EBF811F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3508DF12-D3DE-4956-ABAB-31D7321B72C7} diff --git a/src/Altinn.Correspondence.Integrations/Hangfire/DependencyInjection.cs b/src/Altinn.Correspondence.Integrations/Hangfire/DependencyInjection.cs index 613c25dd1..b483ac6ac 100644 --- a/src/Altinn.Correspondence.Integrations/Hangfire/DependencyInjection.cs +++ b/src/Altinn.Correspondence.Integrations/Hangfire/DependencyInjection.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using Serilog.Core; namespace Altinn.Correspondence.Integrations.Hangfire; public static class DependencyInjection @@ -26,7 +25,7 @@ public static void ConfigureHangfire(this IServiceCollection services) provider.GetRequiredService(), provider.GetRequiredService>()) ); - }); + }); services.AddHangfireServer(options => options.SchedulePollingInterval = TimeSpan.FromSeconds(2)); } } diff --git a/tools/Altinn.Correspondence.Dashboard/Altinn.Correspondence.Dashboard.csproj b/tools/Altinn.Correspondence.Dashboard/Altinn.Correspondence.Dashboard.csproj new file mode 100644 index 000000000..5955bf08b --- /dev/null +++ b/tools/Altinn.Correspondence.Dashboard/Altinn.Correspondence.Dashboard.csproj @@ -0,0 +1,25 @@ + + + + net9.0 + enable + enable + db15b22b-b1d8-435c-962f-0c7350056033 + Linux + ..\.. + + + + + + + + + + + + + + + + diff --git a/tools/Altinn.Correspondence.Dashboard/Dockerfile b/tools/Altinn.Correspondence.Dashboard/Dockerfile new file mode 100644 index 000000000..af14f3dd2 --- /dev/null +++ b/tools/Altinn.Correspondence.Dashboard/Dockerfile @@ -0,0 +1,30 @@ +# 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:9.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 2526 +ENV ASPNETCORE_URLS=http://+:2526 + +# This stage is used to build the service project +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src +COPY ["tools/Altinn.Correspondence.Dashboard/Altinn.Correspondence.Dashboard.csproj", "tools/Altinn.Correspondence.Dashboard/"] +COPY ["src/Altinn.Correspondence.Persistence/Altinn.Correspondence.Persistence.csproj", "src/Altinn.Correspondence.Persistence/"] +COPY ["src/Altinn.Correspondence.Integrations/Altinn.Correspondence.Integrations.csproj", "src/Altinn.Correspondence.Integrations/"] + +RUN dotnet restore "tools/Altinn.Correspondence.Dashboard/Altinn.Correspondence.Dashboard.csproj" +COPY . . +WORKDIR "/src/tools/Altinn.Correspondence.Dashboard" +RUN dotnet build "Altinn.Correspondence.Dashboard.csproj" -c Release -o /app/build + +# This stage is used to publish the service project to be copied to the final stage +FROM build AS publish +RUN dotnet publish "Altinn.Correspondence.Dashboard.csproj" -c Release -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 . +ENTRYPOINT ["dotnet", "Altinn.Correspondence.Dashboard.dll"] \ No newline at end of file diff --git a/tools/Altinn.Correspondence.Dashboard/HangfireDashboardAuthorizationFilter.cs b/tools/Altinn.Correspondence.Dashboard/HangfireDashboardAuthorizationFilter.cs new file mode 100644 index 000000000..ec6c410d4 --- /dev/null +++ b/tools/Altinn.Correspondence.Dashboard/HangfireDashboardAuthorizationFilter.cs @@ -0,0 +1,11 @@ +using Hangfire.Annotations; +using Hangfire.Dashboard; + +internal class HangfireDashboardAuthorizationFilter : IDashboardAuthorizationFilter +{ + // Dummy implementation. Handled by Container App. + public bool Authorize([NotNull] DashboardContext context) + { + return true; + } +} \ No newline at end of file diff --git a/tools/Altinn.Correspondence.Dashboard/Program.cs b/tools/Altinn.Correspondence.Dashboard/Program.cs new file mode 100644 index 000000000..08819e976 --- /dev/null +++ b/tools/Altinn.Correspondence.Dashboard/Program.cs @@ -0,0 +1,36 @@ +using Altinn.Correspondence.Integrations.Hangfire; +using Altinn.Correspondence.Persistence; +using Hangfire; +using Hangfire.PostgreSql; +using Microsoft.ApplicationInsights.AspNetCore.Extensions; + +var builder = WebApplication.CreateBuilder(args); +builder.Configuration + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", true, true) + .AddJsonFile("appsettings.local.json", true, true); +builder.Services.AddPersistence(builder.Configuration); +builder.Services.AddApplicationInsightsTelemetry(new ApplicationInsightsServiceOptions() +{ + EnableAdaptiveSampling = false +}); +builder.Services.AddSingleton(); +builder.Services.AddHangfire((provider, config) => + { + config.UsePostgreSqlStorage( + c => c.UseConnectionFactory(provider.GetService()) + ); + config.UseSerilogLogProvider(); +}); + +var app = builder.Build(); + +app.UseHttpsRedirection(); + +app.UseHangfireDashboard("/hangfire", new DashboardOptions() +{ + Authorization = [new HangfireDashboardAuthorizationFilter()] +}); +app.MapGet("/", () => Results.Redirect("/hangfire")); + +app.Run(); \ No newline at end of file diff --git a/tools/Altinn.Correspondence.Dashboard/Properties/launchSettings.json b/tools/Altinn.Correspondence.Dashboard/Properties/launchSettings.json new file mode 100644 index 000000000..515aca614 --- /dev/null +++ b/tools/Altinn.Correspondence.Dashboard/Properties/launchSettings.json @@ -0,0 +1,49 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5097" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7224;http://localhost:5097" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "80", + "ASPNETCORE_HTTP_PORTS": "81" + }, + "publishAllPorts": true, + "useSSL": true + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:44756", + "sslPort": 44305 + } + } +} \ No newline at end of file diff --git a/tools/Altinn.Correspondence.Dashboard/appsettings.Development.json b/tools/Altinn.Correspondence.Dashboard/appsettings.Development.json new file mode 100644 index 000000000..ef0ea24b5 --- /dev/null +++ b/tools/Altinn.Correspondence.Dashboard/appsettings.Development.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "DatabaseOptions": { + "ConnectionString": "Host=host.docker.internal:5432;Username=postgres;Password=postgres;Database=correspondence;Pooling=true;Maximum Pool Size=20;Minimum Pool Size=0;" + } +} diff --git a/tools/Altinn.Correspondence.Dashboard/appsettings.json b/tools/Altinn.Correspondence.Dashboard/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/tools/Altinn.Correspondence.Dashboard/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}