├── deno.json ├── .vscode └── settings.json ├── test ├── Altinn.Auth.AuditLog.Tests │ ├── Usings.cs │ ├── AsyncLock.cs │ ├── Mocks │ │ ├── AuthorizationEventRepositoryMock.cs │ │ └── AuthenticationEventRepositoryMock.cs │ ├── Altinn.Auth.AuditLog.Tests.csproj │ ├── Health │ │ └── HealthCheckTests.cs │ ├── AsyncConcurrencyLimiter.cs │ ├── WebApplicationFixture.cs │ ├── WebApplicationTests.cs │ ├── Integration │ │ └── PartitionCreationHostedServiceIntegrationTests.cs │ ├── Singleton.cs │ ├── Controllers │ │ └── AuthenticationEventControllerTest.cs │ ├── PartitionCreationHosterServiceTests.cs │ ├── AuthorizationEventTests.cs │ └── DbFixture.cs └── Altinn.Auth.AuditLog.Functions.Tests │ ├── Usings.cs │ ├── Altinn.Auth.AuditLog.Functions.Tests.csproj │ ├── Functions │ ├── EventsProcessorTest.cs │ └── AuthorizationEventsProcessorTest.cs │ ├── Utils │ └── AssertionUtil.cs │ ├── Helpers │ └── TestDataHelper.cs │ └── Clients │ └── AuditLogClientTest.cs ├── add-q-msg.png ├── fa-output.png ├── auditog-fa-run-1.png ├── src ├── Altinn.Auth.AuditLog.Persistence │ ├── Migration │ │ ├── v0.00 │ │ │ ├── 04-setup-timescaledb.sql │ │ │ ├── 01-setup-schemas.sql │ │ │ ├── 05-setup-hypertable.sql │ │ │ ├── 02-setup-grants.sql │ │ │ └── 03-setup-tables.sql │ │ ├── v0.01 │ │ │ ├── 01-setup-schemas.sql │ │ │ ├── 04-setup-hypertable.sql │ │ │ ├── 03-setup-grants.sql │ │ │ └── 02-setup-tables.sql │ │ ├── v0.03 │ │ │ └── 01-alter-table.sql │ │ ├── _pre │ │ │ └── README.md │ │ ├── _erase │ │ │ └── README.md │ │ ├── _post │ │ │ └── README.md │ │ ├── _init │ │ │ └── README.md │ │ ├── _draft │ │ │ └── README.md │ │ └── v0.02 │ │ │ ├── 00-default-privileges.sql │ │ │ └── 01-setup-tables.sql │ ├── Configuration │ │ └── PostgreSQLSettings.cs │ ├── Altinn.Auth.AuditLog.Persistence.csproj │ ├── PartitionManagerRepository.cs │ ├── AuthenticationEventRepository.cs │ ├── Extensions │ │ └── AuditLogDependencyInjectionExtensions.cs │ └── AuthorizationEventRepository.cs ├── Altinn.Auth.AuditLog │ ├── appsettings.json │ ├── appsettings.AT21.json │ ├── appsettings.AT22.json │ ├── appsettings.AT23.json │ ├── appsettings.AT24.json │ ├── appsettings.PROD.json │ ├── appsettings.TT02.json │ ├── appsettings.YT01.json │ ├── appsettings.Development.json │ ├── Configuration │ │ ├── CustomTelemetryInitializer.cs │ │ └── ConsoleTraceService.cs │ ├── Filters │ │ └── ValidationFilterAttribute.cs │ ├── Program.cs │ ├── Health │ │ ├── HealthCheck.cs │ │ └── HealthTelemetryFilter.cs │ ├── Dockerfile │ ├── Properties │ │ └── launchSettings.json │ ├── Controllers │ │ ├── AuthorizationEventController.cs │ │ └── AuthenticationEventController.cs │ ├── Altinn.Auth.AuditLog.csproj │ ├── AuditLogHost.cs │ └── Services │ │ └── PartitionCreationHostedService.cs ├── Functions │ └── Altinn.Auth.AuditLog.Functions │ │ ├── host.json │ │ ├── Configuration │ │ └── PlatformSettings.cs │ │ ├── Program.cs │ │ ├── Clients │ │ ├── Interfaces │ │ │ └── IAuditLogClient.cs │ │ └── AuditLogClient.cs │ │ ├── EventsProcessor.cs │ │ ├── Altinn.Auth.AuditLog.Functions.csproj │ │ ├── AuthorizationEventsProcessor.cs │ │ └── .gitignore ├── Altinn.Auth.AuditLog.Core │ ├── Enum │ │ ├── AuthenticationEventType.cs │ │ ├── SecurityLevel.cs │ │ ├── XacmlContextDecision.cs │ │ └── AuthenticationMethod.cs │ ├── Altinn.Auth.AuditLog.Core.csproj │ ├── Models │ │ ├── KeyVaultSettings.cs │ │ ├── Partition.cs │ │ ├── ValidationErrorResult.cs │ │ ├── ContextRequest.cs │ │ ├── AuthenticationEvent.cs │ │ └── AuthorizationEvent.cs │ ├── Repositories │ │ └── Interfaces │ │ │ ├── IAuthorizationEventRepository.cs │ │ │ ├── IAuthenticationEventRepository.cs │ │ │ └── IPartitionManagerRepository.cs │ └── Services │ │ ├── Interfaces │ │ ├── IAuthorizationEventService.cs │ │ └── IAuthenticationEventService.cs │ │ ├── AuthenticationEventService.cs │ │ └── AuthorizationEventService.cs └── .dockerignore ├── .github ├── pr-labeler.yml ├── actions │ ├── release │ │ ├── releaseversion.mts │ │ └── action.yml │ └── deploy │ │ ├── action.yml │ │ └── deploy.mts ├── release-drafter.yml └── workflows │ ├── pr-labeler.yml │ ├── create-release-draft.yml │ ├── manual-build-deploy-function-app-to-environment.yml │ ├── build-analyze.yml │ ├── manual-build-deploy-to-environment.yml │ ├── build-deploy-at.yml │ ├── codeql.yml │ └── deploy-after-release.yml ├── .editorconfig ├── Dockerfile ├── renovate.json ├── LICENSE └── Altinn.Auth.AuditLog.sln /deno.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true 3 | } 4 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Functions.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /add-q-msg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/altinn/altinn-auth-audit-log/main/add-q-msg.png -------------------------------------------------------------------------------- /fa-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/altinn/altinn-auth-audit-log/main/fa-output.png -------------------------------------------------------------------------------- /auditog-fa-run-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/altinn/altinn-auth-audit-log/main/auditog-fa-run-1.png -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Migration/v0.00/04-setup-timescaledb.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS timescaledb; -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Migration/v0.01/01-setup-schemas.sql: -------------------------------------------------------------------------------- 1 | -- Schema: authorization 2 | CREATE SCHEMA IF NOT EXISTS authz; 3 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Migration/v0.03/01-alter-table.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE authz.eventlogv1 2 | ADD subject_party_uuid VARCHAR(255); 3 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Migration/v0.00/01-setup-schemas.sql: -------------------------------------------------------------------------------- 1 | -- Schema: authentication 2 | CREATE SCHEMA IF NOT EXISTS authentication; 3 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Migration/_pre/README.md: -------------------------------------------------------------------------------- 1 | # The `_pre` directory 2 | Pre migration scripts. Executed every time before any version. 3 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Migration/_erase/README.md: -------------------------------------------------------------------------------- 1 | # The `_erase` directory 2 | Database cleanup scripts. Executed once only when you do `yuniql erase`. -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Migration/_post/README.md: -------------------------------------------------------------------------------- 1 | # The `_post` directory 2 | Post migration scripts. Executed every time and always the last batch to run. -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Tests/AsyncLock.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Auth.AuditLog.Tests; 2 | 3 | internal sealed class AsyncLock() 4 | : AsyncConcurrencyLimiter(1) 5 | { 6 | } 7 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Migration/_init/README.md: -------------------------------------------------------------------------------- 1 | # The `_init` directory 2 | Initialization scripts. Executed once. This is called the first time you do `yuniql run`. -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Migration/v0.01/04-setup-hypertable.sql: -------------------------------------------------------------------------------- 1 | SELECT create_hypertable('authz.eventlog','created', chunk_time_interval => INTERVAL '1 year', migrate_data => true); -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Migration/v0.00/05-setup-hypertable.sql: -------------------------------------------------------------------------------- 1 | SELECT create_hypertable('authentication.eventlog','created', chunk_time_interval => INTERVAL '1 year', migrate_data => true); -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /.github/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | feature: ['feature/*', 'feat/*'] 2 | bugfix: fix/* 3 | infrastructure: infrastructure/* 4 | automation: automation/* 5 | documentation: documentation/* 6 | enhancement: enhancement/* 7 | dependencies: ['renovate/*'] 8 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/appsettings.AT21.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyVaultSettings": { 3 | "SecretUri": "https://altinn-at21-auditlog-kv.vault.azure.net/" 4 | }, 5 | "kvSetting:SecretUri": "https://altinn-at21-auditlog-kv.vault.azure.net/" 6 | } 7 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/appsettings.AT22.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyVaultSettings": { 3 | "SecretUri": "https://altinn-at22-auditlog-kv.vault.azure.net/" 4 | }, 5 | "kvSetting:SecretUri": "https://altinn-at22-auditlog-kv.vault.azure.net/" 6 | } 7 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/appsettings.AT23.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyVaultSettings": { 3 | "SecretUri": "https://altinn-at23-auditlog-kv.vault.azure.net/" 4 | }, 5 | "kvSetting:SecretUri": "https://altinn-at23-auditlog-kv.vault.azure.net/" 6 | } 7 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/appsettings.AT24.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyVaultSettings": { 3 | "SecretUri": "https://altinn-at24-auditlog-kv.vault.azure.net/" 4 | }, 5 | "kvSetting:SecretUri": "https://altinn-at24-auditlog-kv.vault.azure.net/" 6 | } 7 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/appsettings.PROD.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyVaultSettings": { 3 | "SecretUri": "https://altinn-prod-auditlog-kv.vault.azure.net/" 4 | }, 5 | "kvSetting:SecretUri": "https://altinn-prod-auditlog-kv.vault.azure.net/" 6 | } 7 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/appsettings.TT02.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyVaultSettings": { 3 | "SecretUri": "https://altinn-tt02-auditlog-kv.vault.azure.net/" 4 | }, 5 | "kvSetting:SecretUri": "https://altinn-tt02-auditlog-kv.vault.azure.net/" 6 | } 7 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/appsettings.YT01.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyVaultSettings": { 3 | "SecretUri": "https://altinn-yt01-auditlog-kv.vault.azure.net/" 4 | }, 5 | "kvSetting:SecretUri": "https://altinn-yt01-auditlog-kv.vault.azure.net/" 6 | } 7 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Migration/_draft/README.md: -------------------------------------------------------------------------------- 1 | # The `_draft` directory 2 | Scripts in progress. Scripts that you are currently working and have not moved to specific version directory yet. Executed every time after the latest version. -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Migration/v0.01/03-setup-grants.sql: -------------------------------------------------------------------------------- 1 | GRANT USAGE ON SCHEMA authz TO "${APP-USER}"; 2 | GRANT SELECT,INSERT,UPDATE,REFERENCES,DELETE,TRUNCATE,TRIGGER ON ALL TABLES IN SCHEMA authz TO "${APP-USER}"; 3 | GRANT ALL ON ALL SEQUENCES IN SCHEMA authz TO "${APP-USER}"; 4 | -------------------------------------------------------------------------------- /src/Functions/Altinn.Auth.AuditLog.Functions/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Migration/v0.00/02-setup-grants.sql: -------------------------------------------------------------------------------- 1 | GRANT USAGE ON SCHEMA authentication TO "${APP-USER}"; 2 | GRANT SELECT,INSERT,UPDATE,REFERENCES,DELETE,TRUNCATE,TRIGGER ON ALL TABLES IN SCHEMA authentication TO "${APP-USER}"; 3 | GRANT ALL ON ALL SEQUENCES IN SCHEMA authentication TO "${APP-USER}"; 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | 13 | [*.cs] 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Core/Enum/AuthenticationEventType.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Auth.AuditLog.Core.Enum 2 | { 3 | /// 4 | /// Enumeration for authentication event types 5 | /// 6 | public enum AuthenticationEventType 7 | { 8 | Authenticate = 1, 9 | Refresh = 2, 10 | TokenExchange = 3, 11 | Logout = 4, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Core/Altinn.Auth.AuditLog.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /.github/actions/release/releaseversion.mts: -------------------------------------------------------------------------------- 1 | function getVersion() 2 | { 3 | const currentDate = new Date(); 4 | const year = String(currentDate.getFullYear()).padStart(4, '0'); 5 | const month = String(currentDate.getMonth() + 1).padStart(2, '0'); 6 | const day = String(currentDate.getDate()).padStart(2, '0'); 7 | 8 | const release_version_number = `${year}.${month}.${day}`; 9 | return release_version_number; 10 | } 11 | 12 | console.log(getVersion()); 13 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "Altinn:Npgsql:auditlog": { 9 | "ConnectionString": "Host=localhost;Port=5432;Username=auth_auditlog;Password=Password;Database=authauditlogdb", 10 | "Migrate:ConnectionString": "Host=localhost;Port=5432;Username=auth_auditlog_admin;Password=Password;Database=authauditlogdb" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Functions/Altinn.Auth.AuditLog.Functions/Configuration/PlatformSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Auth.AuditLog.Functions.Configuration 2 | { 3 | /// 4 | /// Represents a set of configuration options when communicating with the platform API. 5 | /// 6 | public class PlatformSettings 7 | { 8 | /// 9 | /// Gets or sets the url for the audit log API endpoint. 10 | /// 11 | public string AuditLogApiEndpoint { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Core/Models/KeyVaultSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Altinn.Auth.AuditLog.Core.Models 8 | { 9 | /// 10 | /// General configuration settings 11 | /// 12 | public class KeyVaultSettings 13 | { 14 | /// 15 | /// Gets or sets the secret uri 16 | /// 17 | public string SecretUri { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Core/Repositories/Interfaces/IAuthorizationEventRepository.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Altinn.Auth.AuditLog.Core.Repositories.Interfaces 9 | { 10 | /// 11 | /// Interface for PostgresSQL operations on authorization event 12 | /// 13 | public interface IAuthorizationEventRepository 14 | { 15 | Task InsertAuthorizationEvent(AuthorizationEvent authorizationEvent); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Core/Models/Partition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Altinn.Auth.AuditLog.Core.Models 8 | { 9 | /// 10 | /// Used for partition creation 11 | /// 12 | public sealed record Partition 13 | { 14 | public required string Name { get; set; } 15 | public required DateOnly StartDate { get; set; } 16 | public required DateOnly EndDate { get; set;} 17 | public required string SchemaName { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | # Changelog 3 | $CHANGES 4 | 5 | See details of [all code changes](https://github.com/altinn/altinn-auth-audit-log/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION) since previous release 6 | 7 | categories: 8 | - title: '🚀 Features' 9 | labels: 10 | - 'kind/feature-request' 11 | - title: '🐛 Bug Fixes' 12 | labels: 13 | - 'kind/bug' 14 | - 'bugfix' 15 | - title: '🧰 Maintenance' 16 | labels: 17 | - 'kind/documentation' 18 | - 'dependencies' 19 | - 'enhancement' 20 | - 'kind/chore' 21 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 22 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Tests/Mocks/AuthorizationEventRepositoryMock.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Models; 2 | using Altinn.Auth.AuditLog.Core.Repositories.Interfaces; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Altinn.Auth.AuditLog.Tests.Mocks 10 | { 11 | public class AuthorizationEventRepositoryMock : IAuthorizationEventRepository 12 | { 13 | public Task InsertAuthorizationEvent(AuthorizationEvent authorizationEvent) 14 | { 15 | return Task.FromResult(authorizationEvent); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Tests/Mocks/AuthenticationEventRepositoryMock.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Models; 2 | using Altinn.Auth.AuditLog.Core.Repositories.Interfaces; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Altinn.Auth.AuditLog.Tests.Mocks 10 | { 11 | public class AuthenticationEventRepositoryMock : IAuthenticationEventRepository 12 | { 13 | public Task InsertAuthenticationEvent(AuthenticationEvent authenticationEvent) 14 | { 15 | return Task.FromResult(authenticationEvent); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | name: PR Labeler 2 | on: 3 | pull_request: 4 | types: [opened] 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | pr-labeler: 11 | permissions: 12 | contents: read # for TimonVS/pr-labeler-action to read config file 13 | pull-requests: write # for TimonVS/pr-labeler-action to add labels in PR 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: TimonVS/pr-labeler-action@f9c084306ce8b3f488a8f3ee1ccedc6da131d1af # v5 17 | with: 18 | repo-token: ${{ secrets.GITHUB_TOKEN }} 19 | configuration-path: .github/pr-labeler.yml # optional, .github/pr-labeler.yml is the default value 20 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Migration/v0.02/00-default-privileges.sql: -------------------------------------------------------------------------------- 1 | -- grant on tables 2 | ALTER DEFAULT PRIVILEGES FOR USER "${YUNIQL-USER}" IN SCHEMA authentication GRANT 3 | SELECT 4 | , INSERT, UPDATE, REFERENCES, DELETE, TRUNCATE, TRIGGER ON TABLES TO "${APP-USER}"; 5 | 6 | ALTER DEFAULT PRIVILEGES FOR USER "${YUNIQL-USER}" IN SCHEMA authz GRANT 7 | SELECT 8 | , INSERT, UPDATE, REFERENCES, DELETE, TRUNCATE, TRIGGER ON TABLES TO "${APP-USER}"; 9 | 10 | -- grant on sequences 11 | ALTER DEFAULT PRIVILEGES FOR USER "${YUNIQL-USER}" IN SCHEMA authentication GRANT ALL ON SEQUENCES TO "${APP-USER}"; 12 | 13 | ALTER DEFAULT PRIVILEGES FOR USER "${YUNIQL-USER}" IN SCHEMA authz GRANT ALL ON SEQUENCES TO "${APP-USER}"; 14 | -------------------------------------------------------------------------------- /src/Functions/Altinn.Auth.AuditLog.Functions/Program.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Functions.Clients; 2 | using Altinn.Auth.AuditLog.Functions.Clients.Interfaces; 3 | using Altinn.Auth.AuditLog.Functions.Configuration; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | 8 | var host = new HostBuilder() 9 | .ConfigureFunctionsWorkerDefaults() 10 | .ConfigureServices(s=> 11 | { 12 | s.AddOptions().Configure((settings, configuration) => 13 | { 14 | configuration.GetSection("Platform").Bind(settings); 15 | }); 16 | s.AddHttpClient(); 17 | }) 18 | .Build(); 19 | 20 | host.Run(); 21 | -------------------------------------------------------------------------------- /.github/workflows/create-release-draft.yml: -------------------------------------------------------------------------------- 1 | name: Schedule release and draft release notes 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "0 0 * * 3" 6 | 7 | jobs: 8 | releaseversion: 9 | name: releaseversion 10 | runs-on: ubuntu-latest 11 | 12 | permissions: 13 | packages: write 14 | contents: write # for release-drafter/release-drafter to create a github release 15 | pull-requests: write # for release-drafter/release-drafter to add label to PR 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 20 | - name: Create release draft 21 | uses: ./.github/actions/release 22 | with: 23 | github-token: ${{ secrets.GITHUB_TOKEN }} 24 | 25 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Core/Services/Interfaces/IAuthorizationEventService.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Altinn.Auth.AuditLog.Core.Services.Interfaces 9 | { 10 | /// 11 | /// service for authentication events 12 | /// 13 | public interface IAuthorizationEventService 14 | { 15 | /// 16 | /// Logs an authentication event 17 | /// 18 | /// the authorization event 19 | /// 20 | public Task CreateAuthorizationEvent(AuthorizationEvent authorizationEvent); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Core/Services/Interfaces/IAuthenticationEventService.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Altinn.Auth.AuditLog.Core.Services.Interfaces 9 | { 10 | /// 11 | /// service for authentication events 12 | /// 13 | public interface IAuthenticationEventService 14 | { 15 | /// 16 | /// Logs an authentication event 17 | /// 18 | /// the authentication event 19 | /// 20 | public Task CreateAuthenticationEvent(AuthenticationEvent authenticationEvent); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Core/Repositories/Interfaces/IAuthenticationEventRepository.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Altinn.Auth.AuditLog.Core.Repositories.Interfaces 9 | { 10 | /// 11 | /// Interface for PostgresSQL operations on authentication event 12 | /// 13 | public interface IAuthenticationEventRepository 14 | { 15 | /// 16 | /// inserts an authentication event to the database 17 | /// 18 | /// 19 | /// 20 | Task InsertAuthenticationEvent(AuthenticationEvent authenticationEvent); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/Configuration/CustomTelemetryInitializer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ApplicationInsights.Channel; 2 | using Microsoft.ApplicationInsights.Extensibility; 3 | 4 | namespace Altinn.Auth.AuditLog.Configuration 5 | { 6 | /// 7 | /// Set up custom telemetry for Application Insights 8 | /// 9 | public class CustomTelemetryInitializer : ITelemetryInitializer 10 | { 11 | /// 12 | /// Custom TelemetryInitializer that sets some specific values for the component 13 | /// 14 | public void Initialize(ITelemetry telemetry) 15 | { 16 | if (string.IsNullOrEmpty(telemetry.Context.Cloud.RoleName)) 17 | { 18 | telemetry.Context.Cloud.RoleName = "auth-auditlog"; 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/Filters/ValidationFilterAttribute.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Filters; 2 | using Microsoft.AspNetCore.Mvc; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace Altinn.Auth.AuditLog.Filters 6 | { 7 | [ExcludeFromCodeCoverage] 8 | public class ValidationFilterAttribute : IActionFilter 9 | { 10 | public void OnActionExecuting(ActionExecutingContext context) 11 | { 12 | if (!context.ModelState.IsValid) 13 | { 14 | context.Result = new UnprocessableEntityObjectResult(context.ModelState); 15 | } 16 | } 17 | 18 | /// 19 | /// Post execution 20 | /// 21 | /// context 22 | public void OnActionExecuted(ActionExecutedContext context) {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/Program.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog; 2 | using Microsoft.IdentityModel.Logging; 3 | using System; 4 | 5 | WebApplication app = AuditLogHost.Create(args); 6 | 7 | app.AddDefaultAltinnMiddleware(errorHandlingPath: "/auditlog/api/v1/error"); 8 | Console.WriteLine($"The application environment: {app.Environment.EnvironmentName}"); 9 | 10 | if (app.Environment.IsDevelopment()) 11 | { 12 | // Enable higher level of detail in exceptions related to JWT validation 13 | IdentityModelEventSource.ShowPII = true; 14 | 15 | // Enable Swagger 16 | app.UseSwagger(); 17 | app.UseSwaggerUI(); 18 | } 19 | 20 | app.UseAuthentication(); 21 | 22 | app.MapDefaultAltinnEndpoints(); 23 | app.MapControllers(); 24 | 25 | app.Run(); 26 | 27 | /// 28 | /// Startup class. 29 | /// 30 | public partial class Program 31 | { 32 | } 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Building the auditlog api 2 | FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine@sha256:f271ed7d0fd9c5a7ed0acafed8a2bc978bb65c19dcd2eeea0415adef142ffc87 AS build 3 | ARG SOURCE_REVISION_ID=LOCALBUILD 4 | 5 | COPY src . 6 | WORKDIR Altinn.Auth.AuditLog/ 7 | RUN echo "Building Altinn.Auth.AuditLog with SourceRevisionId=${SOURCE_REVISION_ID}" 8 | RUN dotnet build Altinn.Auth.AuditLog.csproj -c Release -o /app_output -p SourceRevisionId=${SOURCE_REVISION_ID} 9 | RUN dotnet publish Altinn.Auth.AuditLog.csproj -c Release -o /app_output -p SourceRevisionId=${SOURCE_REVISION_ID} 10 | 11 | # Building the final image 12 | FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine@sha256:5e8dca92553951e42caed00f2568771b0620679f419a28b1335da366477d7f98 AS final 13 | EXPOSE 5166 14 | WORKDIR /app 15 | COPY --from=build /app_output . 16 | RUN mkdir /tmp/logtelemetry 17 | ENTRYPOINT ["dotnet", "Altinn.Auth.AuditLog.dll"] 18 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>Altinn/renovate-config" 5 | ], 6 | "packageRules": [ 7 | { 8 | "matchPackageNames": [ 9 | "System.IdentityModel.Tokens.Jwt", 10 | "Microsoft.IdentityModel.Tokens", 11 | "Microsoft.IdentityModel.Logging" 12 | ], 13 | "groupName": "IdentityModel Extensions for .NET", 14 | "groupSlug": "azure-activedirectory-identitymodel-extensions-for-dotnet", 15 | "matchUpdateTypes": [ 16 | "major" 17 | ] 18 | }, 19 | { 20 | "matchPackageNames": [ 21 | "Testcontainers", 22 | "Testcontainers.PostgreSql" 23 | ], 24 | "groupName": "Testcontainers for .NET", 25 | "groupSlug": "testcontainers-for-dotnet", 26 | "matchUpdateTypes": [ 27 | "major" 28 | ] 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Core/Enum/SecurityLevel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Altinn.Auth.AuditLog.Core.Enum 7 | { 8 | /// 9 | /// This holds information about different types of authentication levels available in Altinn. 10 | /// 11 | public enum SecurityLevel 12 | { 13 | /// Security Level 0 (Self identified) 14 | SelfIdentifed = 0, 15 | 16 | /// Security Level 1 (static password) 17 | NotSensitive = 1, 18 | 19 | /// Security Level 2 (pin code) 20 | QuiteSensitive = 2, 21 | 22 | /// Security Level 3 23 | Sensitive = 3, 24 | 25 | /// Security Level 4 (buypass) 26 | VerySensitive = 4 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Functions/Altinn.Auth.AuditLog.Functions/Clients/Interfaces/IAuditLogClient.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Models; 2 | using System.Buffers; 3 | 4 | namespace Altinn.Auth.AuditLog.Functions.Clients.Interfaces 5 | { 6 | public interface IAuditLogClient 7 | { 8 | /// 9 | /// Posts an authentication event to the auditlog api. 10 | /// 11 | /// The authevent to be created 12 | Task SaveAuthenticationEvent(AuthenticationEvent authEvent, CancellationToken cancellationToken); 13 | 14 | /// 15 | /// Posts an authorization event to the auditlog api. 16 | /// 17 | /// The authorization event to be created 18 | Task SaveAuthorizationEvent(ReadOnlySequence authorizationEvent, CancellationToken cancellationToken); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Core/Models/ValidationErrorResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Altinn.Auth.AuditLog.Core.Models 8 | { 9 | /// 10 | /// Response model for any errors occuring during processing of a model in the core service layer 11 | /// 12 | public class ValidationErrorResult 13 | { 14 | /// 15 | /// Gets or sets a value indicating whether the processing was a success i.e. no errors has been added 16 | /// 17 | public bool IsValid 18 | { 19 | get { return Errors.Count == 0; } 20 | } 21 | 22 | /// 23 | /// Gets or sets a dictionary of errors occured during processing 24 | /// 25 | public Dictionary Errors { get; set; } = new Dictionary(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/Health/HealthCheck.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Diagnostics.HealthChecks; 2 | 3 | namespace Altinn.Auth.AuditLog.Health 4 | { 5 | /// 6 | /// Health check service configured in startup. See https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks 7 | /// Listen to 8 | /// 9 | public class HealthCheck : IHealthCheck 10 | { 11 | /// 12 | /// Verifies the health status 13 | /// 14 | /// The healtcheck context 15 | /// The cancellationtoken 16 | /// The health check result 17 | public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) 18 | { 19 | return Task.FromResult( 20 | HealthCheckResult.Healthy("A healthy result.")); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Core/Repositories/Interfaces/IPartitionManagerRepository.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Altinn.Auth.AuditLog.Core.Repositories.Interfaces 9 | { 10 | /// 11 | /// Interface for PostgresSQL operations on partition management 12 | /// 13 | public interface IPartitionManagerRepository 14 | { 15 | /// 16 | /// Checks and creates necessary partition for authentication event table 17 | /// 18 | /// the list of partitions to be created 19 | /// A . 20 | /// true if the partition is created 21 | Task CreatePartitions(IReadOnlyList partitions, CancellationToken cancellationToken = default); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/actions/release/action.yml: -------------------------------------------------------------------------------- 1 | name: "Schedule a release draft" 2 | description: "Schedule a release draft every wednesday at 00:00 with version as date" 3 | inputs: 4 | github-token: 5 | description: "The github token" 6 | required: true 7 | type: string 8 | 9 | runs: 10 | using: "composite" 11 | steps: 12 | - uses: denoland/setup-deno@v1 13 | with: 14 | deno-version: v1.x 15 | 16 | - name: Get version number 17 | id: get_version_number 18 | shell: bash 19 | run: | 20 | echo "RELEASE_VERSION=$(deno run -A ./.github/actions/release/releaseversion.mts)" >> $GITHUB_OUTPUT 21 | 22 | # Drafts your next Release notes as Pull Requests are merged into "master" 23 | - uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6 24 | env: 25 | GITHUB_TOKEN: ${{ inputs.github-token }} 26 | with: 27 | tag: ${{ steps.get_version_number.outputs.RELEASE_VERSION }} 28 | version: ${{ steps.get_version_number.outputs.RELEASE_VERSION }} 29 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/Dockerfile: -------------------------------------------------------------------------------- 1 | #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. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:9.0@sha256:bf48e8b328707fae0e63a1b7d764d770221def59b97468c8cdee68f4e38ddfb9 AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | EXPOSE 443 7 | 8 | FROM mcr.microsoft.com/dotnet/sdk:9.0@sha256:ca77338a19f87a7d24494a3656cb7d878a040c158621337b9cd3ab811c5eb057 AS build 9 | WORKDIR /src 10 | COPY ["Altinn.Auth.AuditLog/Altinn.Auth.AuditLog.csproj", "Altinn.Auth.AuditLog/"] 11 | RUN dotnet restore "Altinn.Auth.AuditLog/Altinn.Auth.AuditLog.csproj" 12 | COPY . . 13 | WORKDIR "/src/Altinn.Auth.AuditLog" 14 | RUN dotnet build "Altinn.Auth.AuditLog.csproj" -c Release -o /app/build 15 | 16 | FROM build AS publish 17 | RUN dotnet publish "Altinn.Auth.AuditLog.csproj" -c Release -o /app/publish /p:UseAppHost=false 18 | 19 | FROM base AS final 20 | WORKDIR /app 21 | COPY --from=publish /app/publish . 22 | ENTRYPOINT ["dotnet", "Altinn.Auth.AuditLog.dll"] 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Altinn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Configuration/PostgreSQLSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Auth.AuditLog.Persistence.Configuration 2 | { 3 | /// 4 | /// Settings for Postgres database 5 | /// 6 | public class PostgreSQLSettings 7 | { 8 | /// 9 | /// Connection string for the postgres db 10 | /// 11 | public string ConnectionString { get; set; } 12 | 13 | /// 14 | /// Password for app user for the postgres db 15 | /// 16 | public string AuthAuditLogDbPwd { get; set; } 17 | 18 | /// 19 | /// Connection string for the postgres db 20 | /// 21 | public string AdminConnectionString { get; set; } 22 | 23 | /// 24 | /// Password for app user for the postgres db 25 | /// 26 | public string AuthAuditLogDbAdminPwd { get; set; } 27 | 28 | /// 29 | /// Gets or sets a value indicating whether to include parameter values in logging/tracing. 30 | /// 31 | public bool LogParameters { get; set; } = false; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Core/Models/ContextRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Altinn.Auth.AuditLog.Core.Models 4 | { 5 | /// 6 | /// This model describes an authorization event. An authorization event is an action triggered when a user requests access to an operation 7 | /// 8 | 9 | public class AccessSubject 10 | { 11 | public List Attribute { get; set; } 12 | } 13 | 14 | public class Action 15 | { 16 | public List Attribute { get; set; } 17 | } 18 | 19 | public class Resource 20 | { 21 | public List Attribute { get; set; } 22 | } 23 | 24 | public class Attribute 25 | { 26 | public string Id { get; set; } 27 | public string Value { get; set; } 28 | public string DataType { get; set; } 29 | public bool IncludeInResult { get; set; } 30 | } 31 | 32 | public class ContextRequest 33 | { 34 | public bool ReturnPolicyIdList { get; set; } 35 | public List AccessSubject { get; set; } 36 | public List Action { get; set; } 37 | public List Resources { get; set; } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Altinn.Auth.AuditLog.Persistence.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Functions.Tests/Altinn.Auth.AuditLog.Functions.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Core/Services/AuthenticationEventService.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Models; 2 | using Altinn.Auth.AuditLog.Core.Repositories.Interfaces; 3 | using Altinn.Auth.AuditLog.Core.Services.Interfaces; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace Altinn.Auth.AuditLog.Core.Services 12 | { 13 | /// 14 | public class AuthenticationEventService : IAuthenticationEventService 15 | { 16 | private readonly ILogger _logger; 17 | private readonly IAuthenticationEventRepository _authenticationEventRepository; 18 | 19 | public AuthenticationEventService( 20 | ILogger logger, 21 | IAuthenticationEventRepository authenticationLogRepository) 22 | { 23 | _logger = logger; 24 | _authenticationEventRepository = authenticationLogRepository; 25 | } 26 | 27 | /// 28 | public async Task CreateAuthenticationEvent(AuthenticationEvent authenticationEvent) 29 | { 30 | await _authenticationEventRepository.InsertAuthenticationEvent(authenticationEvent); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Core/Services/AuthorizationEventService.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Models; 2 | using Altinn.Auth.AuditLog.Core.Repositories; 3 | using Altinn.Auth.AuditLog.Core.Repositories.Interfaces; 4 | using Altinn.Auth.AuditLog.Core.Services.Interfaces; 5 | using Microsoft.Extensions.Logging; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | 12 | namespace Altinn.Auth.AuditLog.Core.Services 13 | { 14 | /// 15 | public class AuthorizationEventService : IAuthorizationEventService 16 | { 17 | private readonly ILogger _logger; 18 | private readonly IAuthorizationEventRepository _authorizationEventRepository; 19 | 20 | public AuthorizationEventService( 21 | ILogger logger, 22 | IAuthorizationEventRepository authorizationLogRepository) 23 | { 24 | _logger = logger; 25 | _authorizationEventRepository = authorizationLogRepository; 26 | } 27 | 28 | /// 29 | public async Task CreateAuthorizationEvent(AuthorizationEvent authorizationEvent) 30 | { 31 | await _authorizationEventRepository.InsertAuthorizationEvent(authorizationEvent); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/Health/HealthTelemetryFilter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ApplicationInsights.Channel; 2 | using Microsoft.ApplicationInsights.DataContracts; 3 | using Microsoft.ApplicationInsights.Extensibility; 4 | using System.Diagnostics.CodeAnalysis; 5 | 6 | namespace Altinn.Auth.AuditLog.Health 7 | { 8 | /// 9 | /// Filter to exclude health check request from Application Insights 10 | /// 11 | [ExcludeFromCodeCoverage] 12 | public class HealthTelemetryFilter : ITelemetryProcessor 13 | { 14 | private ITelemetryProcessor Next { get; set; } 15 | 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | public HealthTelemetryFilter(ITelemetryProcessor next) 20 | { 21 | Next = next; 22 | } 23 | 24 | /// 25 | public void Process(ITelemetry item) 26 | { 27 | if (ExcludeItemTelemetry(item)) 28 | { 29 | return; 30 | } 31 | 32 | Next.Process(item); 33 | } 34 | 35 | private bool ExcludeItemTelemetry(ITelemetry item) 36 | { 37 | RequestTelemetry request = item as RequestTelemetry; 38 | 39 | if (request != null && request.Url.ToString().EndsWith("/health/")) 40 | { 41 | return true; 42 | } 43 | 44 | return false; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "http": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "launchUrl": "swagger", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Development" 9 | }, 10 | "dotnetRunMessages": true, 11 | "applicationUrl": "http://localhost:5166" 12 | }, 13 | "https": { 14 | "commandName": "Project", 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | }, 20 | "dotnetRunMessages": true, 21 | "applicationUrl": "https://localhost:7236;http://localhost:5166" 22 | }, 23 | "IIS Express": { 24 | "commandName": "IISExpress", 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "environmentVariables": { 28 | "ASPNETCORE_ENVIRONMENT": "Development" 29 | } 30 | }, 31 | "Docker": { 32 | "commandName": "Docker", 33 | "launchBrowser": true, 34 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", 35 | "publishAllPorts": true, 36 | "useSSL": true 37 | } 38 | }, 39 | "$schema": "https://json.schemastore.org/launchsettings.json", 40 | "iisSettings": { 41 | "windowsAuthentication": false, 42 | "anonymousAuthentication": true, 43 | "iisExpress": { 44 | "applicationUrl": "http://localhost:51380", 45 | "sslPort": 44374 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/Functions/Altinn.Auth.AuditLog.Functions/EventsProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Azure.Messaging; 3 | using System.Text.Json; 4 | using Microsoft.Azure.Functions.Worker; 5 | using Microsoft.Extensions.Logging; 6 | using Altinn.Auth.AuditLog.Functions.Clients.Interfaces; 7 | using System.Text.Json.Serialization; 8 | using Altinn.Auth.AuditLog.Core.Models; 9 | 10 | namespace Altinn.Auth.AuditLog.Functions 11 | { 12 | public class EventsProcessor 13 | { 14 | private readonly ILogger _logger; 15 | private readonly IAuditLogClient _auditLogClient; 16 | 17 | public EventsProcessor(ILogger logger, 18 | IAuditLogClient auditLogClient) 19 | { 20 | _logger = logger; 21 | _auditLogClient = auditLogClient; 22 | } 23 | 24 | /// 25 | /// Reads cloud event from eventlog queue and post it to auditlog api 26 | /// 27 | [Function(nameof(EventsProcessor))] 28 | public async Task Run( 29 | [QueueTrigger("eventlog", Connection = "QueueStorage")] string item, 30 | FunctionContext executionContext, 31 | CancellationToken cancellationToken) 32 | { 33 | var options = new JsonSerializerOptions(); 34 | options.Converters.Add(new JsonStringEnumConverter()); 35 | AuthenticationEvent authEvent = JsonSerializer.Deserialize(item, options); 36 | await _auditLogClient.SaveAuthenticationEvent(authEvent, cancellationToken); 37 | 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Tests/Altinn.Auth.AuditLog.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | all 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Core/Enum/XacmlContextDecision.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Altinn.Auth.AuditLog.Core.Enum 6 | { 7 | /// 8 | /// The element contains the result of policy evaluation. 9 | /// 10 | /// The element is of DecisionType simple type. 11 | /// The values of the element have the following meanings: 12 | /// “Permit”: the requested access is permitted 13 | /// “Deny”: the requested access is denied. 14 | /// “Indeterminate”: the PDP is unable to evaluate the requested access. Reasons for such inability include: missing attributes, network errors while 15 | /// retrieving policies, division by zero during policy evaluation, syntax errors in the decision request or in the policy, etc. 16 | /// “NotApplicable”: the PDP does not have any policy that applies to this decision request. 17 | /// 18 | public enum XacmlContextDecision 19 | { 20 | /// 21 | /// “Permit”: the requested access is permitted 22 | /// 23 | Permit = 1, 24 | 25 | /// 26 | /// “Deny”: the requested access is denied. 27 | /// 28 | Deny = 2, 29 | 30 | /// 31 | /// “Indeterminate”: the PDP is unable to evaluate the requested access. Reasons for such inability include: missing attributes, network errors while retrieving policies, division by zero during policy evaluation, syntax errors in the decision request or in the policy, etc. 32 | /// 33 | Indeterminate = 3, 34 | 35 | /// 36 | /// “NotApplicable”: the PDP does not have any policy that applies to this decision request. 37 | /// 38 | NotApplicable = 4, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/actions/deploy/action.yml: -------------------------------------------------------------------------------- 1 | name: "Deploy audit-log to environment" 2 | description: "Deploy audit-log to a given environment" 3 | inputs: 4 | image-name: 5 | description: "The name of the image to deploy" 6 | required: false 7 | type: string 8 | default: ghcr.io/altinn/altinn-auth-audit-log 9 | image-tag: 10 | description: "The tag of the image to deploy" 11 | required: true 12 | type: string 13 | container-name: 14 | description: "The name of the container in the containerapp" 15 | required: false 16 | type: string 17 | default: auditlog 18 | resource-group: 19 | description: "The name of the resource group in Azure" 20 | required: true 21 | type: string 22 | container-app: 23 | description: "The name of the containerapp in Azure" 24 | required: true 25 | type: string 26 | function-app: 27 | description: "The name of the functionapp in Azure" 28 | required: true 29 | type: string 30 | function-app-path: 31 | description: "The path to the pre-built function app" 32 | required: true 33 | type: string 34 | 35 | runs: 36 | using: "composite" 37 | steps: 38 | - uses: denoland/setup-deno@v1 39 | with: 40 | deno-version: v1.x 41 | 42 | - name: Test 43 | run: az account show 44 | shell: bash 45 | 46 | - name: Deploy Image to ContainerApp 47 | shell: bash 48 | env: 49 | NAME: ${{ inputs.container-app }} 50 | CONTAINER_NAME: ${{ inputs.container-name }} 51 | RESOURCE_GROUP: ${{ inputs.resource-group }} 52 | IMAGE: ${{ inputs.image-name }}:${{ inputs.image-tag }} 53 | FORCE_COLOR: '2' 54 | run: deno run -A ./.github/actions/deploy/deploy.mts 55 | 56 | - name: Deploy FunctionApp 57 | uses: Azure/functions-action@0bd707f87c0b6385742bab336c74e1afc61f6369 # v1 58 | with: 59 | app-name: ${{ inputs.function-app }} 60 | package: ${{ inputs.function-app-path }} 61 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Tests/Health/HealthCheckTests.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Health; 2 | using Microsoft.AspNetCore.Mvc.Testing; 3 | using Microsoft.AspNetCore.TestHost; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Net.Http.Headers; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | 12 | namespace Altinn.Auth.AuditLog.Tests.Health 13 | { 14 | /// 15 | /// Health check 16 | /// 17 | public class HealthCheckTests(DbFixture dbFixture, WebApplicationFixture webApplicationFixture) 18 | : WebApplicationTests(dbFixture, webApplicationFixture) 19 | { 20 | 21 | /// 22 | /// Verify that component responds on health check 23 | /// 24 | /// 25 | [Fact] 26 | public async Task VerifyHealthCheck_OK() 27 | { 28 | using var client = CreateClient(); 29 | 30 | HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "/health"); 31 | 32 | HttpResponseMessage response = await client.SendAsync(httpRequestMessage); 33 | string content = await response.Content.ReadAsStringAsync(); 34 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 35 | } 36 | 37 | /// 38 | /// Verify that component responds on health check 39 | /// 40 | /// 41 | [Fact] 42 | public async Task VerifyAliveCheck_OK() 43 | { 44 | HttpClient client = CreateClient(); 45 | 46 | HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "/alive") 47 | { 48 | }; 49 | 50 | HttpResponseMessage response = await client.SendAsync(httpRequestMessage); 51 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Migration/v0.01/02-setup-tables.sql: -------------------------------------------------------------------------------- 1 | -- Table : authz.Decision 2 | CREATE TABLE IF NOT EXISTS authz.decision 3 | ( 4 | decisionid integer PRIMARY KEY, 5 | name text, 6 | description text 7 | ) 8 | TABLESPACE pg_default; 9 | 10 | GRANT ALL ON TABLE authz.decision TO "${APP-USER}"; 11 | 12 | GRANT ALL ON TABLE authz.decision TO "${YUNIQL-USER}"; 13 | 14 | INSERT INTO authz.decision( 15 | decisionid, name, description) 16 | VALUES (1, 'Permit', 'the requested access is permitted'); 17 | INSERT INTO authz.decision( 18 | decisionid, name, description) 19 | VALUES (2, 'Deny', 'the requested access is denied'); 20 | INSERT INTO authz.decision( 21 | decisionid, name, description) 22 | VALUES (3, 'Indeterminate', 'the PDP is unable to evaluate the requested access. Reasons for such inability include: missing attributes, network errors while retrieving policies, division by zero during policy evaluation, syntax errors in the decision request or in the policy, etc.'); 23 | INSERT INTO authz.decision( 24 | decisionid, name, description) 25 | VALUES (4, 'NotApplicable', 'the PDP does not have any policy that applies to this decision request.'); 26 | 27 | -- Table: authz.eventlog 28 | CREATE TABLE IF NOT EXISTS authz.eventlog 29 | ( 30 | sessionid text, 31 | created timestamp with time zone NOT NULL, 32 | subjectuserid INTEGER, 33 | subjectorgcode text, 34 | subjectorgnumber INTEGER, 35 | subjectparty INTEGER, 36 | resourcepartyid INTEGER, 37 | resource text, 38 | instanceid text, 39 | operation text, 40 | ipaddress text, 41 | contextrequestjson jsonb, 42 | decision integer, 43 | CONSTRAINT authzdecisionid_fkey FOREIGN KEY (decision) 44 | REFERENCES authz.decision (decisionid) MATCH SIMPLE 45 | ON UPDATE NO ACTION 46 | ON DELETE NO ACTION 47 | NOT VALID 48 | ) 49 | TABLESPACE pg_default; 50 | 51 | GRANT ALL ON TABLE authz.eventlog TO "${APP-USER}"; 52 | 53 | GRANT ALL ON TABLE authz.eventlog TO "${YUNIQL-USER}"; 54 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/Controllers/AuthorizationEventController.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Models; 2 | using Altinn.Auth.AuditLog.Core.Services.Interfaces; 3 | using Altinn.Auth.AuditLog.Filters; 4 | using Microsoft.AspNetCore.Mvc; 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace Altinn.Auth.AuditLog.Controllers 8 | { 9 | /// 10 | /// Expose Api endpoints for authorization event log 11 | /// 12 | [ApiController] 13 | public class AuthorizationEventController : ControllerBase 14 | { 15 | private readonly ILogger _logger; 16 | private readonly IAuthorizationEventService _authorizationEventService; 17 | /// 18 | /// Initializes a new instance of the class 19 | /// 20 | public AuthorizationEventController( 21 | ILogger logger, 22 | IAuthorizationEventService authorizationEventService) 23 | { 24 | _logger = logger; 25 | _authorizationEventService = authorizationEventService; 26 | } 27 | 28 | [HttpPost] 29 | [Route("auditlog/api/v1/authorizationevent")] 30 | [Consumes("application/json")] 31 | [Produces("application/json")] 32 | [ProducesResponseType(200)] 33 | [ProducesResponseType(400)] 34 | [ProducesResponseType(500)] 35 | [ServiceFilter(typeof(ValidationFilterAttribute))] 36 | public async Task Post([FromBody] AuthorizationEvent authorizationEvent) 37 | { 38 | try 39 | { 40 | await _authorizationEventService.CreateAuthorizationEvent(authorizationEvent); 41 | return Ok(); 42 | } 43 | catch (Exception ex) 44 | { 45 | _logger.LogError(ex, "Internal exception occurred during logging of authorization event"); 46 | return new ObjectResult(ProblemDetailsFactory.CreateProblemDetails(HttpContext)); 47 | } 48 | 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.github/actions/deploy/deploy.mts: -------------------------------------------------------------------------------- 1 | import { $, chalk } from 'npm:zx'; 2 | 3 | const containerAppName = Deno.env.get('NAME'); 4 | const containerName = Deno.env.get('CONTAINER_NAME'); 5 | const resourceGroup = Deno.env.get('RESOURCE_GROUP'); 6 | const image = Deno.env.get('IMAGE'); 7 | 8 | const latestRevision = await updateContainerImage(); 9 | console.log(`new revision: ${chalk.cyan(latestRevision)}`); 10 | 11 | let healthy = false; 12 | for (let i = 0; i < 15; i++) { 13 | if (i > 0) { 14 | console.log(`Not healty yet (${i}), waiting 30 seconds...`); 15 | await new Promise((resolve) => setTimeout(resolve, 30_000)); 16 | } 17 | 18 | const healthState = await getRevisionHealthState(latestRevision); 19 | console.log(`revision health state: ${healthStateDisplay(healthState)}`); 20 | if (healthState === 'Healthy') { 21 | console.log('Health state is healthy'); 22 | healthy = true; 23 | break; 24 | } 25 | } 26 | 27 | if (!healthy) { 28 | console.log('Health state is not healthy'); 29 | Deno.exit(1); 30 | } 31 | 32 | async function updateContainerImage() { 33 | console.log(`Updating container image to ${chalk.cyan(image)}...`); 34 | const output = await $`az containerapp update \ 35 | --name ${containerAppName} \ 36 | --resource-group ${resourceGroup} \ 37 | --container-name ${containerName} \ 38 | --image ${image}`.quiet(); 39 | console.log('Done.'); 40 | 41 | const parsed = JSON.parse(output.stdout); 42 | // console.log(parsed); 43 | return parsed.properties.latestRevisionName; 44 | } 45 | 46 | async function getRevisionHealthState(revisionName: string) { 47 | const output = await $`az containerapp revision show \ 48 | --name ${containerAppName} \ 49 | --resource-group ${resourceGroup} \ 50 | --revision ${revisionName}`.quiet(); 51 | const parsed = JSON.parse(output.stdout); 52 | return parsed.properties.healthState; 53 | } 54 | 55 | function healthStateDisplay(state: string) { 56 | switch (state) { 57 | case 'Healthy': 58 | return chalk.green(state); 59 | case 'Unhealthy': 60 | return chalk.red(state); 61 | default: 62 | return chalk.cyan(state); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Tests/AsyncConcurrencyLimiter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace Altinn.Auth.AuditLog.Tests; 7 | 8 | internal class AsyncConcurrencyLimiter 9 | : IDisposable 10 | { 11 | private readonly SemaphoreSlim _semaphoreSlim; 12 | 13 | public AsyncConcurrencyLimiter(int maxConcurrency) 14 | { 15 | //Guard.IsGreaterThanOrEqualTo(maxConcurrency, 1); 16 | Assert.True(maxConcurrency > 0); 17 | 18 | _semaphoreSlim = new SemaphoreSlim(maxConcurrency, maxConcurrency); 19 | } 20 | 21 | public void Dispose() 22 | { 23 | _semaphoreSlim.Dispose(); 24 | } 25 | 26 | /// 27 | /// Acquires a lock to access the resource thread-safe. 28 | /// 29 | /// An that releases the lock on . 30 | public async Task Acquire() 31 | { 32 | await _semaphoreSlim.WaitAsync(); 33 | return new Ticket(_semaphoreSlim); 34 | } 35 | 36 | /// 37 | /// A lock to synchronize threads. 38 | /// 39 | private sealed class Ticket : IDisposable 40 | { 41 | private SemaphoreSlim? _semaphoreSlim; 42 | 43 | /// 44 | /// Initializes a new instance of the class. 45 | /// 46 | /// The semaphore slim to synchronize threads. 47 | public Ticket(SemaphoreSlim semaphoreSlim) 48 | { 49 | _semaphoreSlim = semaphoreSlim; 50 | } 51 | 52 | ~Ticket() 53 | { 54 | if (_semaphoreSlim != null) 55 | { 56 | //ThrowHelper.ThrowInvalidOperationException("Lock not released."); 57 | throw new InvalidOperationException("Lock not released."); 58 | } 59 | } 60 | 61 | public void Dispose() 62 | { 63 | GC.SuppressFinalize(this); 64 | Interlocked.Exchange(ref _semaphoreSlim, null)?.Release(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Migration/v0.02/01-setup-tables.sql: -------------------------------------------------------------------------------- 1 | 2 | -- Table: authentication.eventlogv1 3 | CREATE TABLE IF NOT EXISTS authentication.eventlogv1 4 | ( 5 | sessionid text, 6 | externalsessionid text, 7 | subscriptionkey text, 8 | externaltokenissuer text, 9 | created timestamp with time zone NOT NULL, 10 | userid integer, 11 | supplierid text, 12 | orgnumber integer, 13 | eventtypeid integer, 14 | authenticationmethodid integer, 15 | authenticationlevelid integer, 16 | ipaddress text, 17 | isauthenticated boolean NOT NULL, 18 | CONSTRAINT authenticationeventtype_fkey FOREIGN KEY (eventtypeid) 19 | REFERENCES authentication.authenticationeventtype (authenticationeventtypeid) MATCH SIMPLE 20 | ON UPDATE NO ACTION 21 | ON DELETE NO ACTION 22 | NOT VALID, 23 | CONSTRAINT authenticationlevel_fkey FOREIGN KEY (authenticationlevelid) 24 | REFERENCES authentication.authenticationlevel (authenticationlevelid) MATCH SIMPLE 25 | ON UPDATE NO ACTION 26 | ON DELETE NO ACTION 27 | NOT VALID, 28 | CONSTRAINT authenticationmethod_fkey FOREIGN KEY (authenticationmethodid) 29 | REFERENCES authentication.authenticationmethod (authenticationmethodid) MATCH SIMPLE 30 | ON UPDATE NO ACTION 31 | ON DELETE NO ACTION 32 | NOT VALID 33 | ) PARTITION BY RANGE (created); 34 | 35 | CREATE INDEX ON authentication.eventlogv1 (created); 36 | 37 | -- Table: authz.eventlogv1 38 | CREATE TABLE IF NOT EXISTS authz.eventlogv1 39 | ( 40 | sessionid text, 41 | created timestamp with time zone NOT NULL, 42 | subjectuserid INTEGER, 43 | subjectorgcode text, 44 | subjectorgnumber INTEGER, 45 | subjectparty INTEGER, 46 | resourcepartyid INTEGER, 47 | resource text, 48 | instanceid text, 49 | operation text, 50 | ipaddress text, 51 | contextrequestjson jsonb, 52 | decision integer, 53 | CONSTRAINT authzdecisionid_fkey FOREIGN KEY (decision) 54 | REFERENCES authz.decision (decisionid) MATCH SIMPLE 55 | ON UPDATE NO ACTION 56 | ON DELETE NO ACTION 57 | NOT VALID 58 | ) PARTITION BY RANGE (created); 59 | 60 | CREATE INDEX ON authz.eventlogv1 (created); 61 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/Controllers/AuthenticationEventController.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Models; 2 | using Altinn.Auth.AuditLog.Core.Services.Interfaces; 3 | using Altinn.Auth.AuditLog.Filters; 4 | using Microsoft.AspNetCore.Http.HttpResults; 5 | using Microsoft.AspNetCore.Mvc; 6 | using System.ComponentModel.DataAnnotations; 7 | 8 | namespace Altinn.Auth.AuditLog.Controllers 9 | { 10 | /// 11 | /// Expose Api endpoints for authentication log 12 | /// 13 | [ApiController] 14 | public class AuthenticationEventController : ControllerBase 15 | { 16 | private readonly ILogger _logger; 17 | private readonly IAuthenticationEventService _authenticationEventService; 18 | /// 19 | /// Initializes a new instance of the class 20 | /// 21 | public AuthenticationEventController( 22 | ILogger logger, 23 | IAuthenticationEventService authenticationEventService) 24 | { 25 | _logger = logger; 26 | _authenticationEventService = authenticationEventService; 27 | } 28 | 29 | [HttpPost] 30 | [Route("auditlog/api/v1/authenticationevent")] 31 | [Consumes("application/json")] 32 | [Produces("application/json")] 33 | [ProducesResponseType(200)] 34 | [ProducesResponseType(400)] 35 | [ProducesResponseType(500)] 36 | [ServiceFilter(typeof(ValidationFilterAttribute))] 37 | public async Task Post([FromBody] AuthenticationEvent authenticationEvent) 38 | { 39 | try 40 | { 41 | await _authenticationEventService.CreateAuthenticationEvent(authenticationEvent); 42 | return Ok(); 43 | } 44 | catch (Exception ex) 45 | { 46 | _logger.LogError(ex, "Internal exception occurred during logging of authentication event"); 47 | return new ObjectResult(ProblemDetailsFactory.CreateProblemDetails(HttpContext)); 48 | } 49 | 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Tests/WebApplicationFixture.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.AspNetCore.Mvc.Testing; 7 | using Microsoft.AspNetCore.TestHost; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Options; 11 | using Microsoft.Extensions.Time.Testing; 12 | using Xunit; 13 | 14 | namespace Altinn.Auth.AuditLog.Tests; 15 | 16 | public class WebApplicationFixture 17 | : IAsyncLifetime 18 | { 19 | private readonly WebApplicationFactory _factory = new(); 20 | 21 | Task IAsyncLifetime.InitializeAsync() 22 | { 23 | return Task.CompletedTask; 24 | } 25 | 26 | async Task IAsyncLifetime.DisposeAsync() 27 | { 28 | await _factory.DisposeAsync(); 29 | } 30 | 31 | public WebApplicationFactory CreateServer( 32 | Action? configureConfiguration = null, 33 | Action? configureServices = null) 34 | { 35 | return _factory.WithWebHostBuilder(builder => 36 | { 37 | if (configureConfiguration is not null) 38 | { 39 | var settings = new ConfigurationBuilder(); 40 | configureConfiguration(settings); 41 | builder.UseConfiguration(settings.Build()); 42 | } 43 | 44 | if (configureServices is not null) 45 | { 46 | builder.ConfigureTestServices(configureServices); 47 | } 48 | }); 49 | } 50 | 51 | private class WebApplicationFactory : WebApplicationFactory 52 | { 53 | protected override void ConfigureWebHost(IWebHostBuilder builder) 54 | { 55 | builder.ConfigureTestServices(services => 56 | { 57 | var timeProvider = new FakeTimeProvider(); 58 | services.AddSingleton(timeProvider); 59 | services.AddSingleton(timeProvider); 60 | }); 61 | 62 | base.ConfigureWebHost(builder); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/Altinn.Auth.AuditLog.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | cd7f3135-73f2-4dbb-a8df-a928ba70d031 8 | Linux 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/Configuration/ConsoleTraceService.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Yuniql.Extensibility; 3 | 4 | namespace Altinn.Auth.AuditLog.Configuration 5 | { 6 | /// 7 | /// Postgres Logging Configuration 8 | /// 9 | [ExcludeFromCodeCoverage] 10 | public class ConsoleTraceService : ITraceService 11 | { 12 | /// 13 | public bool IsDebugEnabled { get; set; } = false; 14 | 15 | /// 16 | public bool IsTraceSensitiveData { get; set; } = false; 17 | 18 | /// 19 | public bool IsTraceToFile { get; set; } = false; 20 | 21 | /// 22 | public bool IsTraceToDirectory { get; set; } = false; 23 | 24 | /// 25 | public string TraceDirectory { get; set; } 26 | 27 | /// 28 | public void Info(string message, object payload = null) 29 | { 30 | var traceMessage = $"INF {DateTime.UtcNow.ToString("o")} {message}{Environment.NewLine}"; 31 | Console.Write(traceMessage); 32 | } 33 | 34 | /// 35 | public void Error(string message, object payload = null) 36 | { 37 | var traceMessage = $"ERR {DateTime.UtcNow.ToString("o")} {message}{Environment.NewLine}"; 38 | Console.Write(traceMessage); 39 | } 40 | 41 | /// 42 | public void Debug(string message, object payload = null) 43 | { 44 | if (IsDebugEnabled) 45 | { 46 | var traceMessage = $"DBG {DateTime.UtcNow.ToString("o")} {message}{Environment.NewLine}"; 47 | Console.Write(traceMessage); 48 | } 49 | } 50 | 51 | /// 52 | public void Success(string message, object payload = null) 53 | { 54 | var traceMessage = $"INF {DateTime.UtcNow.ToString("u")} {message}{Environment.NewLine}"; 55 | Console.Write(traceMessage); 56 | } 57 | 58 | /// 59 | public void Warn(string message, object payload = null) 60 | { 61 | var traceMessage = $"WRN {DateTime.UtcNow.ToString("o")} {message}{Environment.NewLine}"; 62 | Console.Write(traceMessage); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Functions/Altinn.Auth.AuditLog.Functions/Altinn.Auth.AuditLog.Functions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | v4 5 | Exe 6 | enable 7 | enable 8 | f8fe67e3-905c-460e-a481-846283a06965 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | PreserveNewest 33 | 34 | 35 | PreserveNewest 36 | Never 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/AuditLogHost.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Models; 2 | using Altinn.Auth.AuditLog.Core.Services; 3 | using Altinn.Auth.AuditLog.Core.Services.Interfaces; 4 | using Altinn.Auth.AuditLog.Filters; 5 | using Altinn.Auth.AuditLog.Health; 6 | using Altinn.Auth.AuditLog.Persistence.Configuration; 7 | using Altinn.Auth.AuditLog.Services; 8 | using Altinn.Authorization.ServiceDefaults; 9 | using Azure.Identity; 10 | using Azure.Security.KeyVault.Secrets; 11 | using Microsoft.AspNetCore.Mvc.ApiExplorer; 12 | using Microsoft.Extensions.DependencyInjection.Extensions; 13 | using System.Text.Json; 14 | using System.Text.Json.Serialization; 15 | 16 | namespace Altinn.Auth.AuditLog 17 | { 18 | internal static class AuditLogHost 19 | { 20 | /// 21 | /// Configures the auditlog host. 22 | /// 23 | /// The command line arguments. 24 | public static WebApplication Create(string[] args) 25 | { 26 | var builder = AltinnHost.CreateWebApplicationBuilder("auditlog", args); 27 | var services = builder.Services; 28 | var config = builder.Configuration; 29 | 30 | services.AddMemoryCache(); 31 | 32 | services.Configure(config.GetSection("kvSetting")); 33 | builder.AddAuditLogPersistence(); 34 | builder.Services.AddSingleton(); 35 | builder.Services.AddHostedService(sp => sp.GetRequiredService()); 36 | services.AddSingleton(); 37 | services.AddSingleton(); 38 | services.Configure(config.GetSection("PostgreSQLSettings")); 39 | 40 | builder.Services 41 | .AddControllers(options => options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true) 42 | .AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); 43 | builder.Services.AddScoped(); 44 | // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle 45 | builder.Services.AddEndpointsApiExplorer(); 46 | builder.Services.AddSwaggerGen(); 47 | return builder.Build(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Tests/WebApplicationTests.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using Microsoft.AspNetCore.Mvc.Testing; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.Extensions.Time.Testing; 7 | using Npgsql; 8 | using Xunit; 9 | 10 | namespace Altinn.Auth.AuditLog.Tests; 11 | 12 | public abstract class WebApplicationTests 13 | : IClassFixture 14 | , IClassFixture 15 | , IAsyncLifetime 16 | { 17 | private readonly DbFixture _dbFixture; 18 | private readonly WebApplicationFixture _webApplicationFixture; 19 | 20 | public WebApplicationTests(DbFixture dbFixture, WebApplicationFixture webApplicationFixture) 21 | { 22 | _dbFixture = dbFixture; 23 | _webApplicationFixture = webApplicationFixture; 24 | } 25 | 26 | private WebApplicationFactory? _webApp; 27 | private IServiceProvider? _services; 28 | private AsyncServiceScope _scope; 29 | private DbFixture.OwnedDb? _db; 30 | 31 | protected IServiceProvider Services => _scope!.ServiceProvider; 32 | protected NpgsqlDataSource DataSource => Services.GetRequiredService(); 33 | protected FakeTimeProvider TimeProvider => Services.GetRequiredService(); 34 | 35 | protected HttpClient CreateClient() 36 | => _webApp!.CreateClient(); 37 | 38 | protected virtual ValueTask DisposeAsync() 39 | { 40 | return ValueTask.CompletedTask; 41 | } 42 | 43 | protected virtual void ConfigureTestConfiguration(IConfigurationBuilder builder) 44 | { 45 | } 46 | 47 | async Task IAsyncLifetime.DisposeAsync() 48 | { 49 | await DisposeAsync(); 50 | await _scope.DisposeAsync(); 51 | if (_webApp is { } webApp) await webApp.DisposeAsync(); 52 | 53 | if (_db is { } db) await db.DisposeAsync(); 54 | 55 | } 56 | 57 | async Task IAsyncLifetime.InitializeAsync() 58 | { 59 | _db = await _dbFixture.CreateDbAsync(); 60 | _webApp = _webApplicationFixture.CreateServer( 61 | configureConfiguration: config => 62 | { 63 | _db.ConfigureConfiguration(config, "auditlog"); 64 | ConfigureTestConfiguration(config); 65 | }, 66 | configureServices: services => 67 | { 68 | _db.ConfigureServices(services, "auditlog"); 69 | }); 70 | 71 | _services = _webApp.Services; 72 | _scope = _services.CreateAsyncScope(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Tests/Integration/PartitionCreationHostedServiceIntegrationTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Common; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Altinn.Auth.AuditLog.Core.Repositories.Interfaces; 6 | using Altinn.Auth.AuditLog.Persistence; 7 | using Altinn.Auth.AuditLog.Services; 8 | using Altinn.Auth.AuditLog.Tests; 9 | using Microsoft.AspNetCore.Hosting; 10 | using Microsoft.AspNetCore.Routing; 11 | using Microsoft.Extensions.Configuration; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.Logging; 14 | using Moq; 15 | using Npgsql; 16 | using Testcontainers.PostgreSql; 17 | using static System.Net.Mime.MediaTypeNames; 18 | 19 | namespace Altinn.Auth.AuditLog.Tests.Integration; 20 | 21 | public class PartitionCreationHostedServiceIntegrationTests(DbFixture dbFixture, WebApplicationFixture webApplicationFixture) 22 | : WebApplicationTests(dbFixture, webApplicationFixture) 23 | { 24 | protected IPartitionManagerRepository Repository => Services.GetRequiredService(); 25 | protected PartitionCreationHostedService HostedService => Services.GetRequiredService(); 26 | 27 | protected Task WaitForPartitionJob() 28 | { 29 | return HostedService.RunningJob; 30 | } 31 | 32 | [Fact] 33 | public async Task ExecuteAsync_CreatesCurrentMonthPartition_OnlyOnce() 34 | { 35 | TimeProvider.Advance(TimeSpan.FromDays(1) + TimeSpan.FromHours(1)); 36 | await WaitForPartitionJob(); 37 | 38 | // Assert the partition is created 39 | var currentDate = DateOnly.FromDateTime(TimeProvider.GetUtcNow().UtcDateTime); 40 | var currentMonth = currentDate.Month; 41 | var currentYear = currentDate.Year; 42 | var partitionName = $"eventlogv1_y{currentYear}m{currentMonth:D2}"; 43 | 44 | var checkAuthenticationPartitionCommand = $"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema='authentication' and table_name='{partitionName}');"; 45 | await using NpgsqlCommand pgcom = DataSource.CreateCommand(checkAuthenticationPartitionCommand); 46 | var exists = (bool)await pgcom.ExecuteScalarAsync(); 47 | Assert.True(exists); 48 | var checkAuthorizationPartitionCommand = $"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema='authz' and table_name='{partitionName}');"; 49 | await using NpgsqlCommand pgcomAuthz = DataSource.CreateCommand(checkAuthorizationPartitionCommand); 50 | exists = (bool)await pgcomAuthz.ExecuteScalarAsync(); 51 | Assert.True(exists); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Tests/Singleton.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace Altinn.Auth.AuditLog.Tests; 7 | 8 | internal static class Singleton 9 | { 10 | public static Task> Get() 11 | where T : class, IAsyncLifetime, new() 12 | => Ref.Get(); 13 | 14 | public sealed class Ref 15 | : IAsyncDisposable 16 | where T : class, IAsyncLifetime, new() 17 | { 18 | public static async Task> Get() 19 | => new(await Impl.Instance.GetAsync()); 20 | 21 | private int _disposed = 0; 22 | private T _value; 23 | 24 | public T Value => _value; 25 | 26 | private Ref(T value) 27 | { 28 | _value = value; 29 | } 30 | 31 | ~Ref() 32 | { 33 | if (Interlocked.Exchange(ref _disposed, 1) == 0) 34 | { 35 | //ThrowHelper.ThrowInvalidOperationException($"Singleton.Ref<{typeof(T).Name}> was not disposed"); 36 | throw new InvalidOperationException($"Singleton.Ref<{typeof(T).Name}> was not disposed"); 37 | } 38 | } 39 | 40 | public ValueTask DisposeAsync() 41 | { 42 | if (Interlocked.Exchange(ref _disposed, 1) == 0) 43 | { 44 | _value = null!; 45 | GC.SuppressFinalize(this); 46 | return Impl.Instance.DisposeAsync(); 47 | } 48 | 49 | return ValueTask.CompletedTask; 50 | } 51 | } 52 | 53 | private sealed class Impl 54 | : IAsyncDisposable 55 | where T : class, IAsyncLifetime, new() 56 | { 57 | public static readonly Impl Instance = new(); 58 | 59 | private readonly AsyncLock _lock = new(); 60 | 61 | private T? _value; 62 | private int _referenceCount; 63 | 64 | public async ValueTask DisposeAsync() 65 | { 66 | using var guard = await _lock.Acquire(); 67 | if (--_referenceCount == 0) 68 | { 69 | // we just went from 1 reference to 0, so we need to dispose the value 70 | await _value!.DisposeAsync(); 71 | } 72 | } 73 | 74 | public async Task GetAsync() 75 | { 76 | using var guard = await _lock.Acquire(); 77 | if (_referenceCount++ == 0) 78 | { 79 | // we just went from 0 references to 1, so we need to create the value 80 | _value = new T(); 81 | await _value.InitializeAsync(); 82 | } 83 | 84 | return _value!; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Core/Models/AuthenticationEvent.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Enum; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Net.Security; 5 | 6 | namespace Altinn.Auth.AuditLog.Core.Models 7 | { 8 | /// 9 | /// This model describes an authentication event. An authentication event is an action triggered when a user authenticates to altinn 10 | /// 11 | [ExcludeFromCodeCoverage] 12 | public class AuthenticationEvent 13 | { 14 | /// 15 | /// Session Id of the authentication request 16 | /// 17 | public string? SessionId { get; set; } 18 | 19 | /// 20 | /// External Session Id of the authentication request if found 21 | /// 22 | public string? ExternalSessionId { get; set; } 23 | 24 | /// 25 | /// External Token issuer of the authentication request if found 26 | /// 27 | public string? ExternalTokenIssuer { get; set; } 28 | 29 | /// 30 | /// Date and time of the authentication event. Set by producer of logevents 31 | /// 32 | [Required] 33 | public DateTimeOffset? Created { get; set; } 34 | 35 | /// 36 | /// Id of the user that triggered that authentication event 37 | /// 38 | public int? UserId { get; set; } 39 | 40 | /// 41 | /// Relevant if the event is triggered by enterprise user 42 | /// 43 | public string? SupplierId { get; set; } 44 | 45 | /// 46 | /// Relevant if the event is triggered by enterprise user? 47 | /// 48 | public int? OrgNumber { get; set; } 49 | 50 | /// 51 | /// The type of authentication event 52 | /// 53 | public AuthenticationEventType EventType { get; set; } 54 | 55 | /// 56 | /// The type of authentication used by the user (BankId etc) 57 | /// 58 | public AuthenticationMethod? AuthenticationMethod { get; set; } 59 | 60 | /// 61 | /// The level of authentication used by the user (1, 2, etc) 62 | /// 63 | public SecurityLevel? AuthenticationLevel { get; set; } 64 | 65 | /// 66 | /// The session id 67 | /// 68 | public string? IpAddress { get; set; } 69 | 70 | /// 71 | /// The authentication result 72 | /// 73 | public bool IsAuthenticated { get; set; } 74 | 75 | /// 76 | /// Subscription key of the app that triggered the authentiation request 77 | /// 78 | public string? SubscriptionKey { get; set; } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Functions.Tests/Functions/EventsProcessorTest.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Enum; 2 | using Altinn.Auth.AuditLog.Core.Models; 3 | using Altinn.Auth.AuditLog.Functions.Clients.Interfaces; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | using Moq; 6 | 7 | namespace Altinn.Auth.AuditLog.Functions.Tests.Functions 8 | { 9 | public class EventsProcessorTest 10 | { 11 | [Fact] 12 | public async Task Run_ConfirmDeserializationOfAuthenticationEvent() 13 | { 14 | 15 | // Arrange 16 | string serializedAuthenticationEvent = "{\"Created\":\"2023-09-07T06:24:43.971899Z\",\"UserId\":20000003,\"EventType\":\"Authenticate\",\"AuthenticationMethod\":\"BankId\",\"AuthenticationLevel\":\"VerySensitive\"}\r\n"; 17 | 18 | AuthenticationEvent expectedAuthenticationEvent = new AuthenticationEvent() 19 | { 20 | UserId = 20000003, 21 | Created = DateTimeOffset.Parse("2023-09-07T06:24:43.971899Z").UtcDateTime, 22 | AuthenticationMethod = AuthenticationMethod.BankID, 23 | EventType = AuthenticationEventType.Authenticate, 24 | AuthenticationLevel = SecurityLevel.VerySensitive, 25 | }; 26 | 27 | Mock clientMock = new(); 28 | clientMock.Setup(c => c.SaveAuthenticationEvent(It.Is(c => AssertExpectedAuthenticationEvent(c, expectedAuthenticationEvent)), It.IsAny())) 29 | .Returns(Task.CompletedTask); 30 | 31 | EventsProcessor sut = new EventsProcessor(new NullLogger(), clientMock.Object); 32 | 33 | // Act 34 | await sut.Run(serializedAuthenticationEvent, null!, CancellationToken.None); 35 | 36 | // Assert 37 | 38 | clientMock.VerifyAll(); 39 | } 40 | 41 | private static bool AssertExpectedAuthenticationEvent(AuthenticationEvent actualAuthenticationEvent, AuthenticationEvent expectedAuthenticationEvent) 42 | { 43 | Assert.Equal(expectedAuthenticationEvent.AuthenticationLevel, actualAuthenticationEvent.AuthenticationLevel); 44 | Assert.Equal(expectedAuthenticationEvent.AuthenticationMethod, actualAuthenticationEvent.AuthenticationMethod); 45 | Assert.Equal(expectedAuthenticationEvent.Created, actualAuthenticationEvent.Created); 46 | Assert.Equal(expectedAuthenticationEvent.EventType, actualAuthenticationEvent.EventType); 47 | Assert.Equal(expectedAuthenticationEvent.OrgNumber, actualAuthenticationEvent.OrgNumber); 48 | Assert.Equal(expectedAuthenticationEvent.SupplierId, actualAuthenticationEvent.SupplierId); 49 | Assert.Equal(expectedAuthenticationEvent.UserId, actualAuthenticationEvent.UserId); 50 | Assert.Equal(expectedAuthenticationEvent.IpAddress, actualAuthenticationEvent.IpAddress); 51 | return true; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/PartitionManagerRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Data.Common; 5 | using System.Diagnostics.CodeAnalysis; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Altinn.Auth.AuditLog.Core.Models; 10 | using Altinn.Auth.AuditLog.Core.Repositories.Interfaces; 11 | using Altinn.Auth.AuditLog.Persistence.Configuration; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.Logging; 14 | using Microsoft.Extensions.Options; 15 | using Npgsql; 16 | using Yuniql.Core; 17 | 18 | namespace Altinn.Auth.AuditLog.Persistence 19 | { 20 | [ExcludeFromCodeCoverage] 21 | public class PartitionManagerRepository : IPartitionManagerRepository 22 | { 23 | private readonly ILogger _logger; 24 | private readonly NpgsqlDataSource _dataSource; 25 | 26 | /// 27 | /// Initializes a new instance of the class 28 | /// 29 | /// The postgreSQL datasource for AuditLogDB 30 | /// handler for logger service 31 | public PartitionManagerRepository( 32 | [FromKeyedServices(typeof(IPartitionManagerRepository))] NpgsqlDataSource adminDataSource, 33 | ILogger logger) 34 | { 35 | _dataSource = adminDataSource; 36 | _logger = logger; 37 | } 38 | 39 | /// 40 | public async Task CreatePartitions(IReadOnlyList partitions, CancellationToken cancellationToken = default) 41 | { 42 | // Start a batch to execute multiple statements on the same connection 43 | await using (var batch = _dataSource.CreateBatch()) 44 | { 45 | try 46 | { 47 | // Iterate over the list of partitions and create each one 48 | foreach (var partition in partitions) 49 | { 50 | var cmd = batch.CreateBatchCommand(); 51 | cmd.CommandText = /*strpsql*/$""" 52 | CREATE TABLE IF NOT EXISTS {partition.SchemaName}.{partition.Name} 53 | PARTITION OF {partition.SchemaName}.eventlogv1 54 | FOR VALUES FROM ('{partition.StartDate:yyyy-MM-dd}') TO ('{partition.EndDate:yyyy-MM-dd}') 55 | """; 56 | batch.BatchCommands.Add(cmd); 57 | } 58 | 59 | await batch.ExecuteNonQueryAsync(cancellationToken); 60 | 61 | } 62 | catch (Exception ex) 63 | { 64 | _logger.LogError(ex, "AuditLog // PartitionManagerRepository // CreatePartition // Exception"); 65 | throw; 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/manual-build-deploy-function-app-to-environment.yml: -------------------------------------------------------------------------------- 1 | name: Manually build and publish ONLY FUNCTION-APP to a specific environments 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | environment: 6 | type: environment 7 | description: Select the environment 8 | 9 | ref: 10 | type: string 11 | description: The branch or tag to deploy 12 | 13 | env: 14 | DOTNET_VERSION: '9.0.x' 15 | 16 | jobs: 17 | build-function-app: 18 | name: Build Function App 19 | runs-on: windows-latest 20 | 21 | permissions: 22 | contents: read 23 | packages: write 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 28 | with: 29 | ref: ${{ github.event.inputs.ref }} 30 | 31 | - name: Setup .NET 32 | uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5 33 | with: 34 | dotnet-version: ${{ env.DOTNET_VERSION }} 35 | 36 | - name: Restore 37 | shell: bash 38 | working-directory: ./src/Functions/Altinn.Auth.AuditLog.Functions 39 | run: dotnet restore 40 | 41 | - name: Build 42 | shell: bash 43 | working-directory: ./src/Functions/Altinn.Auth.AuditLog.Functions 44 | run: dotnet build --configuration Release --no-restore 45 | 46 | - name: Publish 47 | shell: bash 48 | working-directory: ./src/Functions/Altinn.Auth.AuditLog.Functions 49 | run: dotnet publish --configuration Release --no-build --output ./output 50 | 51 | - name: Upload artifact 52 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 53 | with: 54 | name: function-app 55 | path: ./src/Functions/Altinn.Auth.AuditLog.Functions/output/ 56 | include-hidden-files: true 57 | 58 | deploy: 59 | name: Deploy function app to ${{ inputs.environment }} 60 | runs-on: ubuntu-latest 61 | environment: ${{ inputs.environment }} 62 | needs: 63 | - build-function-app 64 | 65 | permissions: 66 | id-token: write 67 | contents: read 68 | packages: read 69 | 70 | steps: 71 | - name: Checkout 72 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 73 | 74 | - name: Download built function-app 75 | uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5 76 | with: 77 | name: function-app 78 | path: ./artifacts/function-app 79 | 80 | - name: Azure Login 81 | uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 82 | with: 83 | client-id: ${{ secrets.AZURE_CLIENT_ID }} 84 | tenant-id: ${{ secrets.AZURE_TENANT_ID }} 85 | subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} 86 | 87 | - name: Deploy FunctionApp 88 | uses: Azure/functions-action@0bd707f87c0b6385742bab336c74e1afc61f6369 # v1 89 | with: 90 | app-name: ${{ vars.AZURE_FUNCTIONAPP_NAME }} 91 | package: ./artifacts/function-app 92 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Tests/Controllers/AuthenticationEventControllerTest.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Controllers; 2 | using Altinn.Auth.AuditLog.Core.Enum; 3 | using Altinn.Auth.AuditLog.Core.Models; 4 | using System.Net; 5 | using System.Net.Http.Headers; 6 | using System.Text; 7 | using System.Text.Json; 8 | 9 | namespace Altinn.Auth.AuditLog.Tests.Controllers 10 | { 11 | /// 12 | /// Test class for 13 | /// 14 | [Collection("AuthenticationEvent Tests")] 15 | public class AuthenticationEventControllerTest(DbFixture dbFixture, WebApplicationFixture webApplicationFixture) 16 | : WebApplicationTests(dbFixture, webApplicationFixture) 17 | { 18 | private HttpClient CreateEventClient() 19 | { 20 | var client = CreateClient(); 21 | client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 22 | 23 | return client; 24 | } 25 | 26 | [Fact] 27 | public async Task CreateAuthenticationEvent_Ok() 28 | { 29 | using var client = CreateEventClient(); 30 | AuthenticationEvent authenticationEvent = new AuthenticationEvent() 31 | { 32 | Created = TimeProvider.GetUtcNow(), 33 | UserId = 20000003, 34 | AuthenticationMethod = AuthenticationMethod.BankID, 35 | EventType = AuthenticationEventType.Authenticate, 36 | AuthenticationLevel = SecurityLevel.VerySensitive 37 | }; 38 | 39 | string requestUri = "auditlog/api/v1/authenticationevent/"; 40 | 41 | HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri) 42 | { 43 | Content = new StringContent(JsonSerializer.Serialize(authenticationEvent), Encoding.UTF8, "application/json") 44 | }; 45 | 46 | httpRequestMessage.Headers.Add("Accept", "application/json"); 47 | httpRequestMessage.Headers.Add("ContentType", "application/json"); 48 | 49 | HttpResponseMessage response = await client.SendAsync(httpRequestMessage); 50 | 51 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 52 | } 53 | 54 | [Fact] 55 | public async Task CreateAuthenticationEvent_Badrequest_nullobject() 56 | { 57 | AuthenticationEvent authenticationEvent = null; 58 | using var client = CreateEventClient(); 59 | string requestUri = "auditlog/api/v1/authenticationevent/"; 60 | 61 | HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri) 62 | { 63 | Content = new StringContent(JsonSerializer.Serialize(authenticationEvent), Encoding.UTF8, "application/json") 64 | }; 65 | 66 | httpRequestMessage.Headers.Add("Accept", "application/json"); 67 | httpRequestMessage.Headers.Add("ContentType", "application/json"); 68 | 69 | HttpResponseMessage response = await client.SendAsync(httpRequestMessage); 70 | 71 | Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Tests/PartitionCreationHosterServiceTests.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Models; 2 | using Altinn.Auth.AuditLog.Services; 3 | using Microsoft.Extensions.Time.Testing; 4 | using System; 5 | using System.Collections.Generic; 6 | using Xunit; 7 | 8 | namespace Altinn.Auth.AuditLog.Tests; 9 | public class PartitionCreationHostedServiceTests 10 | { 11 | [Fact] 12 | public void GetPartitionsForCurrentAndAdjacentMonths_ReturnsCorrectPartitions() 13 | { 14 | // Arrange 15 | var timeProvider = new FakeTimeProvider(new DateTime(2023, 10, 15)); 16 | var service = new PartitionCreationHostedService(null, null, timeProvider); 17 | 18 | // Act 19 | var partitions = service.GetPartitionsForCurrentAndAdjacentMonths(); 20 | 21 | // Assert 22 | Assert.Equal(6, partitions.Count); 23 | Assert.Contains(partitions, p => p.Name == "eventlogv1_y2023m09" && p.StartDate == new DateOnly(2023, 9, 1) && p.EndDate == new DateOnly(2023, 10, 1)); 24 | Assert.Contains(partitions, p => p.Name == "eventlogv1_y2023m10" && p.StartDate == new DateOnly(2023, 10, 1) && p.EndDate == new DateOnly(2023, 11, 1)); 25 | Assert.Contains(partitions, p => p.Name == "eventlogv1_y2023m11" && p.StartDate == new DateOnly(2023, 11, 1) && p.EndDate == new DateOnly(2023, 12, 1)); 26 | } 27 | 28 | [Theory] 29 | [InlineData(2023, 10, 15, 2023, 10, 1, 2023, 11, 1)] 30 | [InlineData(2023, 2, 1, 2023, 2, 1, 2023, 3, 1)] 31 | [InlineData(2024, 2, 1, 2024, 2, 1, 2024, 3, 1)] 32 | public void GetMonthStartAndEndDate_ReturnsCorrectDates(int year, int month, int day, int expectedStartYear, int expectedStartMonth, int expectedStartDay, int expectedEndYear, int expectedEndMonth, int expectedEndDay) 33 | { 34 | // Arrange 35 | var service = new PartitionCreationHostedService(null, null, null); 36 | var date = new DateOnly(year, month, day); 37 | 38 | // Act 39 | var (startDate, endDate) = service.GetMonthStartAndEndDate(date); 40 | 41 | // Assert 42 | Assert.Equal(new DateOnly(expectedStartYear, expectedStartMonth, expectedStartDay), startDate); 43 | Assert.Equal(new DateOnly(expectedEndYear, expectedEndMonth, expectedEndDay), endDate); 44 | } 45 | 46 | [Fact] 47 | public void GetPartitionsForCurrentAndAdjacentMonths_CrossYearBoundary_ReturnsCorrectPartitions() 48 | { 49 | // Arrange 50 | var timeProvider = new FakeTimeProvider(new DateTime(2023, 12, 15)); 51 | var service = new PartitionCreationHostedService(null, null, timeProvider); 52 | 53 | // Act 54 | var partitions = service.GetPartitionsForCurrentAndAdjacentMonths(); 55 | 56 | // Assert 57 | Assert.Equal(6, partitions.Count); 58 | Assert.Contains(partitions, p => p.Name == "eventlogv1_y2023m11" && p.StartDate == new DateOnly(2023, 11, 1) && p.EndDate == new DateOnly(2023, 12, 1)); 59 | Assert.Contains(partitions, p => p.Name == "eventlogv1_y2023m12" && p.StartDate == new DateOnly(2023, 12, 1) && p.EndDate == new DateOnly(2024, 1, 1)); 60 | Assert.Contains(partitions, p => p.Name == "eventlogv1_y2024m01" && p.StartDate == new DateOnly(2024, 1, 1) && p.EndDate == new DateOnly(2024, 2, 1)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Functions/Altinn.Auth.AuditLog.Functions/Clients/AuditLogClient.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Models; 2 | using Altinn.Auth.AuditLog.Functions.Clients.Interfaces; 3 | using Altinn.Auth.AuditLog.Functions.Configuration; 4 | using CommunityToolkit.HighPerformance; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Extensions.Options; 7 | using System.Buffers; 8 | using System.Net; 9 | using System.Net.Http.Headers; 10 | using System.Net.Http.Json; 11 | using System.Text.Json; 12 | 13 | namespace Altinn.Auth.AuditLog.Functions.Clients; 14 | 15 | /// 16 | /// Integration to Auditlog api 17 | /// 18 | public partial class AuditLogClient 19 | : IAuditLogClient 20 | { 21 | private static readonly MediaTypeHeaderValue _jsonContentType = new("application/json", charSet: "utf-8"); 22 | private static readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web); 23 | 24 | private readonly ILogger _logger; 25 | private readonly HttpClient _client; 26 | 27 | public AuditLogClient( 28 | ILogger logger, 29 | HttpClient client, 30 | IOptions platformSettings) 31 | { 32 | _logger = logger; 33 | _client = client; 34 | _client.BaseAddress = new Uri(platformSettings.Value.AuditLogApiEndpoint); 35 | } 36 | 37 | /// 38 | public async Task SaveAuthenticationEvent(AuthenticationEvent authEvent, CancellationToken cancellationToken) 39 | { 40 | const string ENDPOINT_URL = "auditlog/api/v1/authenticationevent"; 41 | 42 | using var content = JsonContent.Create(authEvent, options: _jsonSerializerOptions); 43 | await PostAuthEventToEndpoint(content, ENDPOINT_URL, cancellationToken); 44 | } 45 | 46 | /// 47 | public async Task SaveAuthorizationEvent(ReadOnlySequence authorizationEvent, CancellationToken cancellationToken) 48 | { 49 | const string ENDPOINT_URL = "auditlog/api/v1/authorizationevent"; 50 | 51 | using var stream = authorizationEvent.AsStream(); 52 | using var content = new StreamContent(stream); 53 | content.Headers.ContentType = _jsonContentType; 54 | await PostAuthEventToEndpoint(content, ENDPOINT_URL, cancellationToken); 55 | } 56 | 57 | private async Task PostAuthEventToEndpoint(HttpContent content, string endpoint, CancellationToken cancellationToken) 58 | { 59 | using HttpResponseMessage response = await _client.PostAsync(endpoint, content, cancellationToken); 60 | if (!response.IsSuccessStatusCode) 61 | { 62 | var responseContent = await response.Content.ReadAsStringAsync(cancellationToken); 63 | Log.RequestFailed(_logger, endpoint, response.StatusCode, responseContent); 64 | throw new HttpRequestException(HttpRequestError.InvalidResponse, $"POST to {endpoint} failed with status code {response.StatusCode}", statusCode: response.StatusCode); 65 | } 66 | } 67 | 68 | private sealed partial class Log 69 | { 70 | [LoggerMessage(1, LogLevel.Error, "POST to {Endpoint} failed with status code {StatusCode}. Response content: {ResponseContent}")] 71 | public static partial void RequestFailed(ILogger logger, string endpoint, HttpStatusCode statusCode, string responseContent); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Core/Enum/AuthenticationMethod.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Altinn.Auth.AuditLog.Core.Enum 7 | { 8 | /// 9 | /// This holds information about different types of authentication methods available in Altinn. 10 | /// 11 | public enum AuthenticationMethod 12 | { 13 | /// 14 | /// The authentication method is not defined 15 | /// 16 | NotDefined = -1, 17 | 18 | /// 19 | /// User is logged in with AltinnPin 20 | /// 21 | AltinnPIN = 0, 22 | 23 | /// 24 | /// User is logged in with BankID 25 | /// 26 | BankID = 1, 27 | 28 | /// 29 | /// User is logged in with help of BuyPass 30 | /// 31 | BuyPass = 2, 32 | 33 | /// 34 | /// User is logged in with help of SAML 35 | /// 36 | SAML2 = 3, 37 | 38 | /// 39 | /// User is logged in with help of SMS pin 40 | /// 41 | SMSPIN = 4, 42 | 43 | /// 44 | /// User is logged in with help of static password 45 | /// 46 | StaticPassword = 5, 47 | 48 | /// 49 | /// User is logged in with help of TaxPIN 50 | /// 51 | TaxPIN = 6, 52 | 53 | /// 54 | /// This value was used until March 2017 for MinIDOTC, BankIDMobil and EIDAS 55 | /// 56 | FederationNotUsedAnymore = 7, 57 | 58 | /// 59 | /// User is logged in with help of Self Identified 60 | /// 61 | SelfIdentified = 8, 62 | 63 | /// 64 | /// User is logged in with help of Enterprise Identified 65 | /// 66 | EnterpriseIdentified = 9, 67 | 68 | /// 69 | /// User is logged in with Commfides 70 | /// 71 | Commfides = 10, 72 | 73 | /// 74 | /// User is logged in with MinID PIN 75 | /// 76 | MinIDPin = 11, 77 | 78 | /// 79 | /// User is logged in with SFTP 80 | /// 81 | OpenSshIdentified = 12, 82 | 83 | /// 84 | /// User is logged in with eIDAS 85 | /// 86 | EIDAS = 13, 87 | 88 | /// 89 | /// User is logged in with BankID mobil 90 | /// 91 | BankIDMobil = 14, 92 | 93 | /// 94 | /// User is logged in with help of IDPORTEN OTC 95 | /// 96 | MinIDOTC = 15, 97 | 98 | /// 99 | /// user is logged in with the help of maskinporten token 100 | /// 101 | MaskinPorten = 16, 102 | 103 | /// 104 | /// user is logged in with the help of virksomhets bruker 105 | /// 106 | VirksomhetsBruker = 17, 107 | 108 | /// 109 | /// user is logged in with the help of testid in idporten 110 | /// 111 | TestID = 18, 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /.github/workflows/build-analyze.yml: -------------------------------------------------------------------------------- 1 | name: Code test and analysis 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | types: [opened, synchronize, reopened] 8 | workflow_dispatch: 9 | jobs: 10 | build-and-test: 11 | name: Build and Test 12 | if: ((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) || github.event_name == 'push') && github.repository_owner == 'Altinn' && github.actor != 'dependabot[bot]' 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 16 | - name: Set inotify watchers 17 | run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p 18 | - name: Set inotify instances 19 | run: echo fs.inotify.max_user_instances=8192 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p 20 | - name: Setup .NET 21 | uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5 22 | with: 23 | dotnet-version: | 24 | 9.0.x 25 | - name: Build & Test 26 | run: | 27 | dotnet build Altinn.Auth.AuditLog.sln -v m 28 | dotnet test Altinn.Auth.AuditLog.sln -v m 29 | analyze: 30 | name: Analyze 31 | if: ((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) || github.event_name == 'push') && github.repository_owner == 'Altinn' && github.actor != 'dependabot[bot]' 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Setup .NET 35 | uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5 36 | with: 37 | dotnet-version: | 38 | 9.0.x 39 | - name: Set up JDK 11 40 | uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5 41 | with: 42 | distribution: "microsoft" 43 | java-version: 17 44 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 45 | with: 46 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 47 | - name: Cache SonarCloud packages 48 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 49 | with: 50 | path: ~\sonar\cache 51 | key: ${{ runner.os }}-sonar 52 | restore-keys: ${{ runner.os }}-sonar 53 | - name: Cache SonarCloud scanner 54 | id: cache-sonar-scanner 55 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 56 | with: 57 | path: .\.sonar\scanner 58 | key: ${{ runner.os }}-sonar-scanner 59 | restore-keys: ${{ runner.os }}-sonar-scanner 60 | - name: Install SonarCloud scanner 61 | if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' 62 | shell: bash 63 | run: | 64 | mkdir -p ./.sonar/scanner 65 | dotnet tool update dotnet-sonarscanner --tool-path ./.sonar/scanner 66 | 67 | - name: Analyze 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 70 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 71 | shell: bash 72 | run: | 73 | set -ex 74 | dotnet tool install --global dotnet-coverage 75 | ./.sonar/scanner/dotnet-sonarscanner begin \ 76 | /k:"Altinn_altinn-auth-audit-log" /o:"altinn" \ 77 | /d:sonar.token="${{ secrets.SONAR_TOKEN }}" \ 78 | /d:sonar.host.url="https://sonarcloud.io" \ 79 | /d:sonar.cs.vstest.reportsPaths="TestResults/**/*.trx" \ 80 | /d:sonar.cs.vscoveragexml.reportsPaths="TestResults/coverage.xml" \ 81 | /d:sonar.cpd.exclusions="**/Swagger/*Filter.cs" 82 | 83 | dotnet build 84 | dotnet coverage collect 'dotnet test --no-build --results-directory TestResults/' -f xml -o 'TestResults/coverage.xml' 85 | 86 | ./.sonar/scanner/dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" 87 | -------------------------------------------------------------------------------- /Altinn.Auth.AuditLog.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.33516.290 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Altinn.Auth.AuditLog", "src\Altinn.Auth.AuditLog\Altinn.Auth.AuditLog.csproj", "{A75621E0-ED35-4E95-943A-6F5135B162A4}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Altinn.Auth.AuditLog.Core", "src\Altinn.Auth.AuditLog.Core\Altinn.Auth.AuditLog.Core.csproj", "{E9ECD8F1-5FBA-4D21-9EA5-C822E81A99F8}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Altinn.Auth.AuditLog.Persistence", "src\Altinn.Auth.AuditLog.Persistence\Altinn.Auth.AuditLog.Persistence.csproj", "{9A08E676-4D92-4D0E-9C88-2B0B50CF662E}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Altinn.Auth.AuditLog.Functions", "src\Functions\Altinn.Auth.AuditLog.Functions\Altinn.Auth.AuditLog.Functions.csproj", "{D4E2AECD-FBED-4B25-967F-D6950ECC7533}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Altinn.Auth.AuditLog.Tests", "test\Altinn.Auth.AuditLog.Tests\Altinn.Auth.AuditLog.Tests.csproj", "{5BDEA074-0191-45FB-86F9-1C7A384CA865}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Altinn.Auth.AuditLog.Functions.Tests", "test\Altinn.Auth.AuditLog.Functions.Tests\Altinn.Auth.AuditLog.Functions.Tests.csproj", "{90D12F35-F9DE-4CB1-9D98-F907E3BBAAF6}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C4B0F39E-5901-4534-AC24-B800959D6B90}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {A75621E0-ED35-4E95-943A-6F5135B162A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {A75621E0-ED35-4E95-943A-6F5135B162A4}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {A75621E0-ED35-4E95-943A-6F5135B162A4}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {A75621E0-ED35-4E95-943A-6F5135B162A4}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {E9ECD8F1-5FBA-4D21-9EA5-C822E81A99F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {E9ECD8F1-5FBA-4D21-9EA5-C822E81A99F8}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {E9ECD8F1-5FBA-4D21-9EA5-C822E81A99F8}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {E9ECD8F1-5FBA-4D21-9EA5-C822E81A99F8}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {9A08E676-4D92-4D0E-9C88-2B0B50CF662E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {9A08E676-4D92-4D0E-9C88-2B0B50CF662E}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {9A08E676-4D92-4D0E-9C88-2B0B50CF662E}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {9A08E676-4D92-4D0E-9C88-2B0B50CF662E}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {D4E2AECD-FBED-4B25-967F-D6950ECC7533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {D4E2AECD-FBED-4B25-967F-D6950ECC7533}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {D4E2AECD-FBED-4B25-967F-D6950ECC7533}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {D4E2AECD-FBED-4B25-967F-D6950ECC7533}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {5BDEA074-0191-45FB-86F9-1C7A384CA865}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {5BDEA074-0191-45FB-86F9-1C7A384CA865}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {5BDEA074-0191-45FB-86F9-1C7A384CA865}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {5BDEA074-0191-45FB-86F9-1C7A384CA865}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {90D12F35-F9DE-4CB1-9D98-F907E3BBAAF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {90D12F35-F9DE-4CB1-9D98-F907E3BBAAF6}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {90D12F35-F9DE-4CB1-9D98-F907E3BBAAF6}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {90D12F35-F9DE-4CB1-9D98-F907E3BBAAF6}.Release|Any CPU.Build.0 = Release|Any CPU 50 | EndGlobalSection 51 | GlobalSection(SolutionProperties) = preSolution 52 | HideSolutionNode = FALSE 53 | EndGlobalSection 54 | GlobalSection(ExtensibilityGlobals) = postSolution 55 | SolutionGuid = {71F04606-4673-4714-8C92-2C035CD3858A} 56 | EndGlobalSection 57 | EndGlobal 58 | -------------------------------------------------------------------------------- /src/Functions/Altinn.Auth.AuditLog.Functions/AuthorizationEventsProcessor.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Functions.Clients.Interfaces; 2 | using CommunityToolkit.HighPerformance; 3 | using Microsoft.Azure.Functions.Worker; 4 | using Microsoft.IO; 5 | using System.Buffers; 6 | using System.Globalization; 7 | using System.IO.Compression; 8 | 9 | namespace Altinn.Auth.AuditLog.Functions; 10 | 11 | public class AuthorizationEventsProcessor 12 | { 13 | private static readonly RecyclableMemoryStreamManager _manager = new(); 14 | 15 | private readonly IAuditLogClient _auditLogClient; 16 | 17 | public AuthorizationEventsProcessor( 18 | IAuditLogClient auditLogClient) 19 | { 20 | _auditLogClient = auditLogClient; 21 | } 22 | 23 | /// 24 | /// Reads authorization event from authorization eventlog queue and post it to auditlog api 25 | /// 26 | [Function(nameof(AuthorizationEventsProcessor))] 27 | public Task Run( 28 | [QueueTrigger("authorizationeventlog", Connection = "QueueStorage")] BinaryData item, 29 | FunctionContext executionContext, 30 | CancellationToken cancellationToken) 31 | { 32 | var raw = item.ToMemory(); 33 | var span = raw.Span; 34 | if (span.Length < 2) 35 | { 36 | return Task.CompletedTask; 37 | } 38 | 39 | var firstBytes = span[0..2]; 40 | if (ushort.TryParse(firstBytes, NumberStyles.None, provider: null, out var version)) 41 | { 42 | var rest = raw[2..]; 43 | return version switch 44 | { 45 | 01 => ProcessV01(rest, cancellationToken), 46 | _ => ProcessInvalidVersion(version, cancellationToken), 47 | }; 48 | } 49 | 50 | return ProcessLegacyVersion(raw, cancellationToken); 51 | } 52 | 53 | // brotli encoded JSON 54 | private async Task ProcessV01(ReadOnlyMemory binaryData, CancellationToken cancellationToken) 55 | { 56 | using var jsonStream = _manager.GetStream(nameof(ProcessV01)); 57 | 58 | { 59 | using var receivedStream = binaryData.AsStream(); 60 | using var decodedStream = new BrotliStream(receivedStream, CompressionMode.Decompress); 61 | decodedStream.CopyTo(jsonStream); // No point in doing async here, as everything is in-memory at this point. 62 | } 63 | 64 | await _auditLogClient.SaveAuthorizationEvent(jsonStream.GetReadOnlySequence(), cancellationToken); 65 | } 66 | 67 | private async Task ProcessLegacyVersion(ReadOnlyMemory base64EncodedJson, CancellationToken cancellationToken) 68 | { 69 | // Check if data starts with `{`, if it does it's not base64 encoded 70 | if (base64EncodedJson.Span[0] == '{') 71 | { 72 | var sequence = new ReadOnlySequence(base64EncodedJson); 73 | await _auditLogClient.SaveAuthorizationEvent(sequence, cancellationToken); 74 | return; 75 | } 76 | 77 | // Data shrinks when decoded from base64, so the original length will fit the decoded byte array 78 | var raw = ArrayPool.Shared.Rent(base64EncodedJson.Length); 79 | 80 | try 81 | { 82 | System.Buffers.Text.Base64.DecodeFromUtf8(base64EncodedJson.Span, raw, out int bytesConsumed, out int bytesWritten); 83 | if (bytesConsumed != base64EncodedJson.Length) 84 | { 85 | throw new InvalidOperationException($"Could not decode entire base64 input (bytes consumed: {bytesConsumed}, input length: {base64EncodedJson.Length})"); 86 | } 87 | 88 | var sequence = new ReadOnlySequence(raw.AsMemory(0, bytesWritten)); 89 | await _auditLogClient.SaveAuthorizationEvent(sequence, cancellationToken); 90 | } 91 | finally 92 | { 93 | ArrayPool.Shared.Return(raw); 94 | } 95 | } 96 | 97 | private async Task ProcessInvalidVersion(ushort version, CancellationToken cancellationToken) 98 | { 99 | throw new InvalidOperationException($"Unsupported authorization event version: {version}"); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Tests/AuthorizationEventTests.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Models; 2 | using System.Text.Json; 3 | 4 | namespace Altinn.Auth.AuditLog.Tests; 5 | 6 | public class AuthorizationEventTests 7 | { 8 | [Fact] 9 | public void Deserialize_Normalizes_ContextRequestJson() 10 | { 11 | var authorizationEvent = new AuthorizationEvent() 12 | { 13 | SubjectUserId = 2000000, 14 | Created = new DateTimeOffset(2018, 05, 15, 02, 05, 00, TimeSpan.Zero), 15 | ResourcePartyId = 1000, 16 | Resource = "taxreport", 17 | InstanceId = "1000/26133fb5-a9f2-45d4-90b1-f6d93ad40713", 18 | Operation = "read", 19 | IpAdress = "192.0.2.1", 20 | ContextRequestJson = JsonSerializer.Deserialize("""{"ReturnPolicyIdList":false,"CombinedDecision":false,"XPathVersion":null,"Attributes":[{"Id":null,"Content":null,"Attributes":[{"Issuer":null,"AttributeId":"urn:altinn:org","IncludeInResult":false,"AttributeValues":[{"Value":"skd","DataType":"http://www.w3.org/2001/XMLSchema#string","Attributes":[{"IsNamespaceDeclaration":false,"Name":{"LocalName":"DataType","Namespace":{"NamespaceName":""},"NamespaceName":""},"NextAttribute":null,"NodeType":2,"PreviousAttribute":null,"Value":"http://www.w3.org/2001/XMLSchema#string","BaseUri":"","Document":null,"Parent":null}],"Elements":[]}]}],"Category":"urn:oasis:names:tc:xacml:1.0:subject-category:access-subject"},{"Id":null,"Content":null,"Attributes":[{"Issuer":null,"AttributeId":"urn:altinn:instance-id","IncludeInResult":false,"AttributeValues":[{"Value":"1000/26133fb5-a9f2-45d4-90b1-f6d93ad40713","DataType":"http://www.w3.org/2001/XMLSchema#string","Attributes":[{"IsNamespaceDeclaration":false,"Name":{"LocalName":"DataType","Namespace":{"NamespaceName":""},"NamespaceName":""},"NextAttribute":null,"NodeType":2,"PreviousAttribute":null,"Value":"http://www.w3.org/2001/XMLSchema#string","BaseUri":"","Document":null,"Parent":null}],"Elements":[]}]},{"Issuer":null,"AttributeId":"urn:altinn:org","IncludeInResult":false,"AttributeValues":[{"Value":"skd","DataType":"http://www.w3.org/2001/XMLSchema#string","Attributes":[],"Elements":[]}]},{"Issuer":null,"AttributeId":"urn:altinn:app","IncludeInResult":false,"AttributeValues":[{"Value":"taxreport","DataType":"http://www.w3.org/2001/XMLSchema#string","Attributes":[],"Elements":[]}]},{"Issuer":null,"AttributeId":"urn:altinn:task","IncludeInResult":false,"AttributeValues":[{"Value":"Task_1","DataType":"http://www.w3.org/2001/XMLSchema#string","Attributes":[],"Elements":[]}]},{"Issuer":null,"AttributeId":"urn:altinn:partyid","IncludeInResult":true,"AttributeValues":[{"Value":"1000","DataType":"http://www.w3.org/2001/XMLSchema#string","Attributes":[],"Elements":[]}]}],"Category":"urn:oasis:names:tc:xacml:3.0:attribute-category:resource"},{"Id":null,"Content":null,"Attributes":[{"Issuer":null,"AttributeId":"urn:oasis:names:tc:xacml:1.0:action:action-id","IncludeInResult":false,"AttributeValues":[{"Value":"read","DataType":"http://www.w3.org/2001/XMLSchema#string","Attributes":[{"IsNamespaceDeclaration":false,"Name":{"LocalName":"DataType","Namespace":{"NamespaceName":""},"NamespaceName":""},"NextAttribute":null,"NodeType":2,"PreviousAttribute":null,"Value":"http://www.w3.org/2001/XMLSchema#string","BaseUri":"","Document":null,"Parent":null}],"Elements":[]}]}],"Category":"urn:oasis:names:tc:xacml:3.0:attribute-category:action"},{"Id":null,"Content":null,"Attributes":[],"Category":"urn:oasis:names:tc:xacml:3.0:attribute-category:environment"}],"RequestReferences":[]}"""), 21 | Decision = Core.Enum.XacmlContextDecision.Permit, 22 | SubjectPartyUuid = "732f9355-c0e4-4df8-98f0-8e773809ff63" 23 | }; 24 | 25 | var raw = authorizationEvent.ContextRequestJson; 26 | authorizationEvent.ContextRequestJson = JsonSerializer.Deserialize(JsonSerializer.Serialize(raw.ToString())); 27 | Assert.Equal(JsonValueKind.String, authorizationEvent.ContextRequestJson.ValueKind); 28 | 29 | var serialized = JsonSerializer.Serialize(authorizationEvent, JsonSerializerOptions.Web); 30 | var deserialized = JsonSerializer.Deserialize(serialized, JsonSerializerOptions.Web); 31 | 32 | Assert.Equal(raw, deserialized!.ContextRequestJson, JsonElement.DeepEquals); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/manual-build-deploy-to-environment.yml: -------------------------------------------------------------------------------- 1 | name: Manually build and publish to a specific environments 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | environment: 6 | type: environment 7 | description: Select the environment 8 | 9 | env: 10 | DOTNET_VERSION: '9.0.x' 11 | 12 | jobs: 13 | build-container-app: 14 | name: Build Container App 15 | runs-on: ubuntu-latest 16 | 17 | permissions: 18 | contents: read 19 | packages: write 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 24 | 25 | - name: Setup .NET 26 | uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5 27 | with: 28 | dotnet-version: ${{ env.DOTNET_VERSION }} 29 | 30 | - name: Set up Docker Buildx 31 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 32 | 33 | - name: Log in to the Container registry 34 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 35 | with: 36 | registry: ghcr.io 37 | username: ${{ github.actor }} 38 | password: ${{ secrets.GITHUB_TOKEN }} 39 | 40 | - name: Build and push Docker image 41 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6 42 | with: 43 | push: true 44 | tags: ghcr.io/altinn/altinn-auth-audit-log:${{ github.sha }} 45 | build-args: | 46 | SOURCE_REVISION_ID=${{ github.sha }} 47 | 48 | build-function-app: 49 | name: Build Function App 50 | runs-on: windows-latest 51 | 52 | permissions: 53 | contents: read 54 | packages: write 55 | 56 | steps: 57 | - name: Checkout 58 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 59 | 60 | - name: Setup .NET 61 | uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5 62 | with: 63 | dotnet-version: ${{ env.DOTNET_VERSION }} 64 | 65 | - name: Restore 66 | shell: bash 67 | working-directory: ./src/Functions/Altinn.Auth.AuditLog.Functions 68 | run: dotnet restore 69 | 70 | - name: Build 71 | shell: bash 72 | working-directory: ./src/Functions/Altinn.Auth.AuditLog.Functions 73 | run: dotnet build --configuration Release --no-restore 74 | 75 | - name: Publish 76 | shell: bash 77 | working-directory: ./src/Functions/Altinn.Auth.AuditLog.Functions 78 | run: dotnet publish --configuration Release --no-build --output ./output 79 | 80 | - name: Upload artifact 81 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 82 | with: 83 | name: function-app 84 | path: ./src/Functions/Altinn.Auth.AuditLog.Functions/output/ 85 | include-hidden-files: true 86 | 87 | deploy: 88 | name: Deploy to ${{ inputs.environment }} 89 | runs-on: ubuntu-latest 90 | environment: ${{ inputs.environment }} 91 | needs: 92 | - build-container-app 93 | - build-function-app 94 | 95 | permissions: 96 | id-token: write 97 | contents: read 98 | packages: read 99 | 100 | steps: 101 | - name: Checkout 102 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 103 | 104 | - name: Download built function-app 105 | uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5 106 | with: 107 | name: function-app 108 | path: ./artifacts/function-app 109 | 110 | - name: Azure Login 111 | uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 112 | with: 113 | client-id: ${{ secrets.AZURE_CLIENT_ID }} 114 | tenant-id: ${{ secrets.AZURE_TENANT_ID }} 115 | subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} 116 | 117 | - uses: ./.github/actions/deploy 118 | name: Deploy 119 | with: 120 | image-tag: ${{ github.sha }} 121 | resource-group: ${{ vars.CONTAINER_APP_RESOURCE_GROUP_NAME }} 122 | container-app: ${{ vars.CONTAINER_APP_NAME }} 123 | function-app: ${{ vars.AZURE_FUNCTIONAPP_NAME }} 124 | function-app-path: ./artifacts/function-app 125 | -------------------------------------------------------------------------------- /.github/workflows/build-deploy-at.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish to AT environments 2 | on: 3 | push: 4 | branches: [main] 5 | workflow_dispatch: 6 | 7 | env: 8 | DOTNET_VERSION: '9.0.x' 9 | 10 | jobs: 11 | build-container-app: 12 | name: Build Container App 13 | runs-on: ubuntu-latest 14 | 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 22 | 23 | - name: Setup .NET 24 | uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5 25 | with: 26 | dotnet-version: ${{ env.DOTNET_VERSION }} 27 | 28 | - name: Set up Docker Buildx 29 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 30 | 31 | - name: Log in to the Container registry 32 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 33 | with: 34 | registry: ghcr.io 35 | username: ${{ github.actor }} 36 | password: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: Build and push Docker image 39 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6 40 | with: 41 | push: true 42 | tags: ghcr.io/altinn/altinn-auth-audit-log:${{ github.sha }} 43 | build-args: | 44 | SOURCE_REVISION_ID=${{ github.sha }} 45 | 46 | build-function-app: 47 | name: Build Function App 48 | runs-on: windows-latest 49 | 50 | permissions: 51 | contents: read 52 | packages: write 53 | 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 57 | 58 | - name: Setup .NET 59 | uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5 60 | with: 61 | dotnet-version: ${{ env.DOTNET_VERSION }} 62 | 63 | - name: Restore 64 | shell: bash 65 | working-directory: ./src/Functions/Altinn.Auth.AuditLog.Functions 66 | run: dotnet restore 67 | 68 | - name: Build 69 | shell: bash 70 | working-directory: ./src/Functions/Altinn.Auth.AuditLog.Functions 71 | run: dotnet build --configuration Release --no-restore 72 | 73 | - name: Publish 74 | shell: bash 75 | working-directory: ./src/Functions/Altinn.Auth.AuditLog.Functions 76 | run: dotnet publish --configuration Release --no-build --output ./output 77 | 78 | - name: Upload artifact 79 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 80 | with: 81 | name: function-app 82 | path: ./src/Functions/Altinn.Auth.AuditLog.Functions/output/ 83 | include-hidden-files: true 84 | 85 | deploy-container-app: 86 | name: Deploy container-app to ${{ matrix.environment }} 87 | runs-on: ubuntu-latest 88 | environment: ${{ matrix.environment }} 89 | needs: 90 | - build-container-app 91 | - build-function-app 92 | 93 | permissions: 94 | id-token: write 95 | contents: read 96 | packages: read 97 | 98 | strategy: 99 | fail-fast: false 100 | matrix: 101 | environment: [AT22, AT23, AT24] 102 | 103 | steps: 104 | - name: Checkout 105 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 106 | 107 | - name: Download built function-app 108 | uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5 109 | with: 110 | name: function-app 111 | path: ./artifacts/function-app 112 | 113 | - name: Azure Login 114 | uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 115 | with: 116 | client-id: ${{ secrets.AZURE_CLIENT_ID }} 117 | tenant-id: ${{ secrets.AZURE_TENANT_ID }} 118 | subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} 119 | 120 | - uses: ./.github/actions/deploy 121 | name: Deploy 122 | with: 123 | image-tag: ${{ github.sha }} 124 | resource-group: ${{ vars.CONTAINER_APP_RESOURCE_GROUP_NAME }} 125 | container-app: ${{ vars.CONTAINER_APP_NAME }} 126 | function-app: ${{ vars.AZURE_FUNCTIONAPP_NAME }} 127 | function-app-path: ./artifacts/function-app 128 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Core/Models/AuthorizationEvent.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Enum; 2 | using System.Buffers; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Text.Json; 6 | using System.Text.Json.Serialization; 7 | 8 | namespace Altinn.Auth.AuditLog.Core.Models; 9 | 10 | /// 11 | /// This model describes an authorization event. An authorization event is an action triggered when a user requests access to an operation 12 | /// 13 | [ExcludeFromCodeCoverage] 14 | public class AuthorizationEvent 15 | { 16 | /// 17 | /// Session Id of the request 18 | /// 19 | public string? SessionId { get; set; } 20 | 21 | /// 22 | /// Date and time of the authorization event. Set by producer of logevents 23 | /// 24 | [Required] 25 | public DateTimeOffset? Created { get; set; } 26 | 27 | /// 28 | /// The userid for the user that requested authorization 29 | /// 30 | public int? SubjectUserId { get; set; } 31 | 32 | /// 33 | /// The org code for the org that requested authorization 34 | /// 35 | public string? SubjectOrgCode { get; set; } 36 | 37 | /// 38 | /// The org number for the org that requested authorization 39 | /// 40 | public int? SubjectOrgNumber { get; set; } 41 | 42 | /// 43 | /// The partyid for the user that requested authorization 44 | /// 45 | public int? SubjectParty { get; set; } 46 | 47 | /// 48 | /// The partyId for resource owner when applicable 49 | /// 50 | public int? ResourcePartyId { get; set; } 51 | 52 | /// 53 | /// The Main resource Id (app, external resource +) 54 | /// 55 | public string? Resource { get; set; } 56 | 57 | /// 58 | /// Instance Id when applicable 59 | /// 60 | public string? InstanceId { get; set; } 61 | 62 | /// 63 | /// Type of operation 64 | /// 65 | public required string Operation { get; set; } 66 | 67 | /// 68 | /// The Ip adress of the calling party 69 | /// 70 | public string? IpAdress { get; set; } 71 | 72 | /// 73 | /// The enriched context request 74 | /// 75 | [JsonConverter(typeof(ContextRequestJsonConverter))] 76 | public required JsonElement ContextRequestJson { get; set; } 77 | 78 | /// 79 | /// Decision for the authorization request 80 | /// 81 | public XacmlContextDecision Decision { get; set; } 82 | 83 | /// 84 | /// The party identifier for the subject 85 | /// 86 | public string? SubjectPartyUuid { get; set; } 87 | 88 | private sealed class ContextRequestJsonConverter 89 | : JsonConverter 90 | { 91 | public override JsonElement Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 92 | { 93 | if (reader.TokenType == JsonTokenType.String) 94 | { 95 | // if we received a string, we should attempt to deserialize it to an object 96 | try 97 | { 98 | return ReadJsonString(ref reader); 99 | } 100 | catch (JsonException) 101 | { 102 | } 103 | } 104 | 105 | return JsonElement.ParseValue(ref reader); 106 | } 107 | 108 | public override void Write(Utf8JsonWriter writer, JsonElement value, JsonSerializerOptions options) 109 | { 110 | value.WriteTo(writer); 111 | } 112 | 113 | private static JsonElement ReadJsonString(ref Utf8JsonReader reader) 114 | { 115 | var length = reader.HasValueSequence ? reader.ValueSequence.Length : reader.ValueSpan.Length; 116 | var buffer = ArrayPool.Shared.Rent(checked((int)length)); 117 | 118 | try 119 | { 120 | var written = reader.CopyString(buffer); 121 | var subReader = new Utf8JsonReader(buffer.AsSpan(0, written)); 122 | return JsonElement.ParseValue(ref subReader); 123 | } 124 | finally 125 | { 126 | ArrayPool.Shared.Return(buffer); 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Functions.Tests/Functions/AuthorizationEventsProcessorTest.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Models; 2 | using Altinn.Auth.AuditLog.Functions.Clients.Interfaces; 3 | using Altinn.Auth.AuditLog.Functions.Tests.Helpers; 4 | using Moq; 5 | using System.Buffers; 6 | using System.Text.Json; 7 | 8 | namespace Altinn.Auth.AuditLog.Functions.Tests.Functions; 9 | 10 | public class AuthorizationEventsProcessorTest 11 | { 12 | [Fact] 13 | public async Task Run_LegacyContent_Base64DecodesAndForwardsToClient() 14 | { 15 | var authorizationEvent = TestDataHelper.GetAuthorizationEvent_LegacyFormat(); 16 | Mock clientMock = CreateMockThatExpectsTestEvent(); 17 | 18 | AuthorizationEventsProcessor sut = new AuthorizationEventsProcessor(clientMock.Object); 19 | 20 | // Act 21 | await sut.Run(authorizationEvent, null!, CancellationToken.None); 22 | 23 | // Assert 24 | clientMock.VerifyAll(); 25 | } 26 | 27 | [Fact] 28 | public async Task Run_LegacyContent_NonBase64_ForwardsToClient() 29 | { 30 | var authorizationEvent = TestDataHelper.GetAuthorizationEvent_LegacyFormat_NonBase64(); 31 | Mock clientMock = CreateMockThatExpectsTestEvent(); 32 | 33 | AuthorizationEventsProcessor sut = new AuthorizationEventsProcessor(clientMock.Object); 34 | 35 | // Act 36 | await sut.Run(authorizationEvent, null!, CancellationToken.None); 37 | 38 | // Assert 39 | clientMock.VerifyAll(); 40 | } 41 | 42 | [Fact] 43 | public async Task Run_V1Content_BrotliDecidesAndForwardsToClient() 44 | { 45 | var authorizationEvent = TestDataHelper.GetAuthorizationEvent_V1Format(); 46 | Mock clientMock = CreateMockThatExpectsTestEvent(); 47 | 48 | AuthorizationEventsProcessor sut = new AuthorizationEventsProcessor(clientMock.Object); 49 | 50 | // Act 51 | await sut.Run(authorizationEvent, null!, CancellationToken.None); 52 | 53 | // Assert 54 | clientMock.VerifyAll(); 55 | } 56 | 57 | [Fact] 58 | public async Task Run_TooSmallMessage_IsIgnored() 59 | { 60 | Mock clientMock = new(); 61 | 62 | AuthorizationEventsProcessor sut = new AuthorizationEventsProcessor(clientMock.Object); 63 | 64 | // Act 65 | await sut.Run(new BinaryData([]), null!, CancellationToken.None); 66 | 67 | // Assert 68 | clientMock.Verify(c => c.SaveAuthorizationEvent(It.IsAny>(), It.IsAny()), Times.Never); 69 | } 70 | 71 | [Fact] 72 | public async Task Run_InvalidVersion_Throws() 73 | { 74 | Mock clientMock = new(); 75 | 76 | AuthorizationEventsProcessor sut = new AuthorizationEventsProcessor(clientMock.Object); 77 | 78 | // Act 79 | await Assert.ThrowsAsync(() => sut.Run(new BinaryData([.. "99abc"u8]), null!, CancellationToken.None)); 80 | 81 | // Assert 82 | clientMock.Verify(c => c.SaveAuthorizationEvent(It.IsAny>(), It.IsAny()), Times.Never); 83 | } 84 | 85 | private Mock CreateMockThatExpectsTestEvent() 86 | { 87 | Mock clientMock = new(); 88 | clientMock.Setup(c => c.SaveAuthorizationEvent(It.IsAny>(), It.IsAny())) 89 | .Callback((ReadOnlySequence data, CancellationToken cancellationToken) => 90 | { 91 | var expectedAuthorizationEvent = TestDataHelper.GetAuthorizationEvent(); 92 | var reader = new Utf8JsonReader(data); 93 | var actualAuthorizationEvent = JsonSerializer.Deserialize(ref reader, JsonSerializerOptions.Web)!; 94 | 95 | Assert.Equal(expectedAuthorizationEvent.InstanceId, actualAuthorizationEvent.InstanceId); 96 | Assert.Equal(expectedAuthorizationEvent.Operation, actualAuthorizationEvent.Operation); 97 | Assert.Equal(expectedAuthorizationEvent.Resource, actualAuthorizationEvent.Resource); 98 | Assert.Equal(expectedAuthorizationEvent.IpAdress, actualAuthorizationEvent.IpAdress); 99 | Assert.Equal(expectedAuthorizationEvent.Created, actualAuthorizationEvent.Created); 100 | Assert.Equal(expectedAuthorizationEvent.ContextRequestJson, actualAuthorizationEvent.ContextRequestJson, JsonElement.DeepEquals); 101 | }) 102 | .Returns(Task.CompletedTask); 103 | 104 | return clientMock; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Functions.Tests/Utils/AssertionUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources; 8 | using Action = Altinn.Auth.AuditLog.Core.Models.Action; 9 | using Resources = Altinn.Auth.AuditLog.Core.Models.Resource; 10 | using Attribute = Altinn.Auth.AuditLog.Core.Models.Attribute; 11 | using Altinn.Auth.AuditLog.Core.Models; 12 | 13 | namespace Altinn.Auth.AuditLog.Functions.Tests.Utils 14 | { 15 | /// 16 | /// Class with methods that can help with assertions of larger objects. 17 | /// 18 | public static class AssertionUtil 19 | { 20 | /// 21 | /// Asserts that two collections of objects have the same property values in the same positions. 22 | /// 23 | /// The Type 24 | /// A collection of expected instances 25 | /// The collection of actual instances 26 | /// The assertion method to be used 27 | public static void AssertCollections(ICollection expected, ICollection actual, Action assertMethod) 28 | { 29 | if (expected == null) 30 | { 31 | Assert.Null(actual); 32 | return; 33 | } 34 | 35 | Assert.Equal(expected.Count, actual.Count); 36 | 37 | Dictionary expectedDict = new Dictionary(); 38 | Dictionary actualDict = new Dictionary(); 39 | 40 | int i = 1; 41 | foreach (T ex in expected) 42 | { 43 | expectedDict.Add(i, ex); 44 | i++; 45 | } 46 | 47 | i = 1; 48 | foreach (T ac in actual) 49 | { 50 | actualDict.Add(i, ac); 51 | i++; 52 | } 53 | 54 | foreach (int key in expectedDict.Keys) 55 | { 56 | assertMethod(expectedDict[key], actualDict[key]); 57 | } 58 | } 59 | 60 | /// 61 | /// Assert that two have the same property in the same positions. 62 | /// 63 | /// An instance with the expected values. 64 | /// The instance to verify. 65 | public static void AssertRuleEqual(Action expected, Action actual) 66 | { 67 | Assert.NotNull(actual); 68 | Assert.NotNull(expected); 69 | 70 | AssertionUtil.AssertCollections(expected.Attribute, actual.Attribute, AssertRuleEqual); 71 | } 72 | 73 | /// 74 | /// Assert that two have the same property in the same positions. 75 | /// 76 | /// An instance with the expected values. 77 | /// The instance to verify. 78 | public static void AssertRuleEqual(AccessSubject expected, AccessSubject actual) 79 | { 80 | Assert.NotNull(actual); 81 | Assert.NotNull(expected); 82 | 83 | AssertionUtil.AssertCollections(expected.Attribute, actual.Attribute, AssertRuleEqual); 84 | } 85 | 86 | /// 87 | /// Assert that two have the same property in the same positions. 88 | /// 89 | /// An instance with the expected values. 90 | /// The instance to verify. 91 | public static void AssertRuleEqual(Resources expected, Resources actual) 92 | { 93 | Assert.NotNull(actual); 94 | Assert.NotNull(expected); 95 | 96 | AssertionUtil.AssertCollections(expected.Attribute, actual.Attribute, AssertRuleEqual); 97 | } 98 | 99 | /// 100 | /// Assert that two have the same property in the same positions. 101 | /// 102 | /// An instance with the expected values. 103 | /// The instance to verify. 104 | public static void AssertRuleEqual(Attribute expected, Attribute actual) 105 | { 106 | Assert.NotNull(actual); 107 | Assert.NotNull(expected); 108 | 109 | Assert.Equal(expected.Id, actual.Id); 110 | Assert.Equal(expected.Value, actual.Value); 111 | Assert.Equal(expected.IncludeInResult, actual.IncludeInResult); 112 | Assert.Equal(expected.DataType, actual.DataType); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL Advanced" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | branches: [ "main" ] 19 | schedule: 20 | - cron: '28 4 * * 3' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze (${{ matrix.language }}) 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners (GitHub.com only) 29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | permissions: 32 | # required for all workflows 33 | security-events: write 34 | 35 | # required to fetch internal or private CodeQL packs 36 | packages: read 37 | 38 | # only required for workflows in private repositories 39 | actions: read 40 | contents: read 41 | 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | include: 46 | - language: csharp 47 | build-mode: none 48 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 49 | # Use `c-cpp` to analyze code written in C, C++ or both 50 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 51 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 52 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 53 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 54 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 55 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 59 | 60 | - name: Setup .NET 8.0.* SDK 61 | uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5 62 | with: 63 | dotnet-version: | 64 | 8.0.x 65 | 9.0.x 66 | 67 | # Initializes the CodeQL tools for scanning. 68 | - name: Initialize CodeQL 69 | uses: github/codeql-action/init@c503cb4fbb5ac9c4ff92454b2613f5bf931403e5 # v3 70 | with: 71 | languages: ${{ matrix.language }} 72 | build-mode: ${{ matrix.build-mode }} 73 | # If you wish to specify custom queries, you can do so here or in a config file. 74 | # By default, queries listed here will override any specified in a config file. 75 | # Prefix the list here with "+" to use these queries and those in the config file. 76 | 77 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 78 | # queries: security-extended,security-and-quality 79 | 80 | # If the analyze step fails for one of the languages you are analyzing with 81 | # "We were unable to automatically build your code", modify the matrix above 82 | # to set the build mode to "manual" for that language. Then modify this step 83 | # to build your code. 84 | # ℹ️ Command-line programs to run using the OS shell. 85 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 86 | - if: matrix.build-mode == 'manual' 87 | shell: bash 88 | run: | 89 | echo 'If you are using a "manual" build mode for one or more of the' \ 90 | 'languages you are analyzing, replace this with the commands to build' \ 91 | 'your code, for example:' 92 | echo ' make bootstrap' 93 | echo ' make release' 94 | exit 1 95 | 96 | - name: Perform CodeQL Analysis 97 | uses: github/codeql-action/analyze@c503cb4fbb5ac9c4ff92454b2613f5bf931403e5 # v3 98 | with: 99 | category: "/language:${{matrix.language}}" 100 | -------------------------------------------------------------------------------- /.github/workflows/deploy-after-release.yml: -------------------------------------------------------------------------------- 1 | run-name: Deploy version ${{ github.event.release.tag_name }} to TT02 and Production 2 | on: 3 | release: 4 | types: [released] 5 | 6 | env: 7 | DOTNET_VERSION: '9.0.x' 8 | REGISTRY: ghcr.io 9 | REPOSITORY: altinn/altinn-auth-audit-log 10 | 11 | 12 | jobs: 13 | tag-image: 14 | name: Tag image 15 | runs-on: ubuntu-latest 16 | 17 | permissions: 18 | contents: read 19 | packages: write 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 24 | 25 | - name: Add Version tag to Docker Image 26 | uses: shrink/actions-docker-registry-tag@f04afd0559f66b288586792eb150f45136a927fa # v4 27 | with: 28 | registry: ${{ env.REGISTRY }} 29 | repository: '${{ env.REPOSITORY }}' 30 | target: ${{ github.sha }} 31 | tags: ${{ github.event.release.tag_name }} 32 | 33 | build-function-app: 34 | name: Build Function App 35 | runs-on: windows-latest 36 | 37 | permissions: 38 | contents: read 39 | packages: write 40 | 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 44 | 45 | - name: Setup .NET 46 | uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5 47 | with: 48 | dotnet-version: ${{ env.DOTNET_VERSION }} 49 | 50 | - name: Restore 51 | shell: bash 52 | working-directory: ./src/Functions/Altinn.Auth.AuditLog.Functions 53 | run: dotnet restore 54 | 55 | - name: Build 56 | shell: bash 57 | working-directory: ./src/Functions/Altinn.Auth.AuditLog.Functions 58 | run: dotnet build --configuration Release --no-restore 59 | 60 | - name: Publish 61 | shell: bash 62 | working-directory: ./src/Functions/Altinn.Auth.AuditLog.Functions 63 | run: dotnet publish --configuration Release --no-build --output ./output 64 | 65 | - name: Upload artifact 66 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 67 | with: 68 | name: function-app 69 | path: ./src/Functions/Altinn.Auth.AuditLog.Functions/output/ 70 | include-hidden-files: true 71 | 72 | deploy-tt02: 73 | name: Deploy to TT02 74 | runs-on: ubuntu-latest 75 | environment: TT02 76 | needs: 77 | - tag-image 78 | - build-function-app 79 | 80 | permissions: 81 | id-token: write 82 | contents: read 83 | packages: read 84 | 85 | steps: 86 | - name: Checkout 87 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 88 | 89 | - name: Download built function-app 90 | uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5 91 | with: 92 | name: function-app 93 | path: ./artifacts/function-app 94 | 95 | - name: Azure Login 96 | uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 97 | with: 98 | client-id: ${{ secrets.AZURE_CLIENT_ID }} 99 | tenant-id: ${{ secrets.AZURE_TENANT_ID }} 100 | subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} 101 | 102 | - uses: ./.github/actions/deploy 103 | name: Deploy 104 | with: 105 | image-tag: ${{ github.sha }} 106 | resource-group: ${{ vars.CONTAINER_APP_RESOURCE_GROUP_NAME }} 107 | container-app: ${{ vars.CONTAINER_APP_NAME }} 108 | function-app: ${{ vars.AZURE_FUNCTIONAPP_NAME }} 109 | function-app-path: ./artifacts/function-app 110 | 111 | deploy-prod: 112 | environment: PROD 113 | runs-on: ubuntu-latest 114 | needs: 115 | - tag-image 116 | - build-function-app 117 | - deploy-tt02 118 | 119 | permissions: 120 | id-token: write 121 | contents: read 122 | packages: read 123 | steps: 124 | - name: Checkout 125 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 126 | 127 | - name: Download built function-app 128 | uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5 129 | with: 130 | name: function-app 131 | path: ./artifacts/function-app 132 | 133 | - name: Azure Login 134 | uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 135 | with: 136 | client-id: ${{ secrets.AZURE_CLIENT_ID }} 137 | tenant-id: ${{ secrets.AZURE_TENANT_ID }} 138 | subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} 139 | 140 | - uses: ./.github/actions/deploy 141 | name: Deploy 142 | with: 143 | image-tag: ${{ github.sha }} 144 | resource-group: ${{ vars.CONTAINER_APP_RESOURCE_GROUP_NAME }} 145 | container-app: ${{ vars.CONTAINER_APP_NAME }} 146 | function-app: ${{ vars.AZURE_FUNCTIONAPP_NAME }} 147 | function-app-path: ./artifacts/function-app 148 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Functions.Tests/Helpers/TestDataHelper.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Models; 2 | using Microsoft.IO; 3 | using System.Diagnostics; 4 | using System.IO.Compression; 5 | using System.Text.Json; 6 | 7 | namespace Altinn.Auth.AuditLog.Functions.Tests.Helpers; 8 | 9 | public static class TestDataHelper 10 | { 11 | private static readonly RecyclableMemoryStreamManager _manager = new(); 12 | 13 | public static AuthorizationEvent GetAuthorizationEvent() 14 | { 15 | AuthorizationEvent authorizationEvent = new AuthorizationEvent() 16 | { 17 | SubjectUserId = 2000000, 18 | Created = new DateTimeOffset(2018, 05, 15, 02, 05, 00, TimeSpan.Zero), 19 | ResourcePartyId = 1000, 20 | Resource = "taxreport", 21 | InstanceId = "1000/26133fb5-a9f2-45d4-90b1-f6d93ad40713", 22 | Operation = "read", 23 | IpAdress = "192.0.2.1", 24 | ContextRequestJson = JsonSerializer.Deserialize("""{"ReturnPolicyIdList":false,"CombinedDecision":false,"XPathVersion":null,"Attributes":[{"Id":null,"Content":null,"Attributes":[{"Issuer":null,"AttributeId":"urn:altinn:org","IncludeInResult":false,"AttributeValues":[{"Value":"skd","DataType":"http://www.w3.org/2001/XMLSchema#string","Attributes":[{"IsNamespaceDeclaration":false,"Name":{"LocalName":"DataType","Namespace":{"NamespaceName":""},"NamespaceName":""},"NextAttribute":null,"NodeType":2,"PreviousAttribute":null,"Value":"http://www.w3.org/2001/XMLSchema#string","BaseUri":"","Document":null,"Parent":null}],"Elements":[]}]}],"Category":"urn:oasis:names:tc:xacml:1.0:subject-category:access-subject"},{"Id":null,"Content":null,"Attributes":[{"Issuer":null,"AttributeId":"urn:altinn:instance-id","IncludeInResult":false,"AttributeValues":[{"Value":"1000/26133fb5-a9f2-45d4-90b1-f6d93ad40713","DataType":"http://www.w3.org/2001/XMLSchema#string","Attributes":[{"IsNamespaceDeclaration":false,"Name":{"LocalName":"DataType","Namespace":{"NamespaceName":""},"NamespaceName":""},"NextAttribute":null,"NodeType":2,"PreviousAttribute":null,"Value":"http://www.w3.org/2001/XMLSchema#string","BaseUri":"","Document":null,"Parent":null}],"Elements":[]}]},{"Issuer":null,"AttributeId":"urn:altinn:org","IncludeInResult":false,"AttributeValues":[{"Value":"skd","DataType":"http://www.w3.org/2001/XMLSchema#string","Attributes":[],"Elements":[]}]},{"Issuer":null,"AttributeId":"urn:altinn:app","IncludeInResult":false,"AttributeValues":[{"Value":"taxreport","DataType":"http://www.w3.org/2001/XMLSchema#string","Attributes":[],"Elements":[]}]},{"Issuer":null,"AttributeId":"urn:altinn:task","IncludeInResult":false,"AttributeValues":[{"Value":"Task_1","DataType":"http://www.w3.org/2001/XMLSchema#string","Attributes":[],"Elements":[]}]},{"Issuer":null,"AttributeId":"urn:altinn:partyid","IncludeInResult":true,"AttributeValues":[{"Value":"1000","DataType":"http://www.w3.org/2001/XMLSchema#string","Attributes":[],"Elements":[]}]}],"Category":"urn:oasis:names:tc:xacml:3.0:attribute-category:resource"},{"Id":null,"Content":null,"Attributes":[{"Issuer":null,"AttributeId":"urn:oasis:names:tc:xacml:1.0:action:action-id","IncludeInResult":false,"AttributeValues":[{"Value":"read","DataType":"http://www.w3.org/2001/XMLSchema#string","Attributes":[{"IsNamespaceDeclaration":false,"Name":{"LocalName":"DataType","Namespace":{"NamespaceName":""},"NamespaceName":""},"NextAttribute":null,"NodeType":2,"PreviousAttribute":null,"Value":"http://www.w3.org/2001/XMLSchema#string","BaseUri":"","Document":null,"Parent":null}],"Elements":[]}]}],"Category":"urn:oasis:names:tc:xacml:3.0:attribute-category:action"},{"Id":null,"Content":null,"Attributes":[],"Category":"urn:oasis:names:tc:xacml:3.0:attribute-category:environment"}],"RequestReferences":[]}"""), 25 | Decision = Core.Enum.XacmlContextDecision.Permit, 26 | SubjectPartyUuid = "732f9355-c0e4-4df8-98f0-8e773809ff63" 27 | }; 28 | 29 | return authorizationEvent; 30 | } 31 | 32 | public static byte[] GetAuthorizationEvent_JsonData() 33 | { 34 | var authorizationEvent = GetAuthorizationEvent(); 35 | 36 | return JsonSerializer.SerializeToUtf8Bytes(authorizationEvent, JsonSerializerOptions.Web); 37 | } 38 | 39 | public static BinaryData GetAuthorizationEvent_LegacyFormat() 40 | { 41 | var utf8Bytes = GetAuthorizationEvent_JsonData(); 42 | var base64Content = Convert.ToBase64String(utf8Bytes); 43 | var binaryData = BinaryData.FromString(base64Content); 44 | 45 | return binaryData; 46 | } 47 | 48 | public static BinaryData GetAuthorizationEvent_LegacyFormat_NonBase64() 49 | { 50 | var utf8Bytes = GetAuthorizationEvent_JsonData(); 51 | var binaryData = BinaryData.FromBytes(utf8Bytes); 52 | 53 | return binaryData; 54 | } 55 | 56 | public static BinaryData GetAuthorizationEvent_V1Format() 57 | { 58 | var authorizationEvent = GetAuthorizationEvent(); 59 | 60 | using var stream = _manager.GetStream(); 61 | stream.Write("01"u8 /* version header */); 62 | 63 | { 64 | using var compressor = new BrotliStream(stream, CompressionLevel.Fastest, leaveOpen: true); 65 | JsonSerializer.Serialize(compressor, authorizationEvent, JsonSerializerOptions.Web); 66 | } 67 | 68 | stream.Position = 0; 69 | byte[] data = new byte[stream.Length]; 70 | var read = stream.Read(data, 0, data.Length); 71 | Debug.Assert(read == data.Length, "Could not read all data from stream."); 72 | 73 | return BinaryData.FromBytes(data); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Functions/Altinn.Auth.AuditLog.Functions/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/AuthenticationEventRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Altinn.Auth.AuditLog.Core.Models; 3 | using Altinn.Auth.AuditLog.Core.Repositories.Interfaces; 4 | using Microsoft.Extensions.Logging; 5 | using Npgsql; 6 | 7 | namespace Altinn.Auth.AuditLog.Persistence 8 | { 9 | [ExcludeFromCodeCoverage] 10 | public class AuthenticationEventRepository : IAuthenticationEventRepository 11 | { 12 | private readonly ILogger _logger; 13 | private readonly NpgsqlDataSource _dataSource; 14 | 15 | /// 16 | /// Initializes a new instance of the class 17 | /// 18 | /// The postgreSQL datasource for AuditLogDB 19 | /// handler for logger service 20 | public AuthenticationEventRepository(NpgsqlDataSource dataSource, 21 | ILogger logger) 22 | { 23 | _dataSource = dataSource; 24 | _logger = logger; 25 | } 26 | 27 | /// 28 | public async Task InsertAuthenticationEvent(AuthenticationEvent authenticationEvent) 29 | { 30 | const string INSERTAUTHNEVENT = /*strpsql*/ 31 | """ 32 | INSERT INTO authentication.eventlogv1 ( 33 | sessionid, 34 | externalsessionid, 35 | subscriptionkey, 36 | externaltokenissuer, 37 | created, 38 | userid, 39 | supplierid, 40 | orgnumber, 41 | eventtypeid, 42 | authenticationmethodid, 43 | authenticationlevelid, 44 | ipaddress, 45 | isauthenticated 46 | ) 47 | VALUES ( 48 | @sessionid, 49 | @externalsessionid, 50 | @subscriptionkey, 51 | @externaltokenissuer, 52 | @created, 53 | @userid, 54 | @supplierid, 55 | @orgnumber, 56 | @eventtypeid, 57 | @authenticationmethodid, 58 | @authenticationlevelid, 59 | @ipaddress, 60 | @isauthenticated 61 | ) 62 | RETURNING *; 63 | """; 64 | 65 | if (authenticationEvent == null) 66 | { 67 | throw new ArgumentNullException(nameof(authenticationEvent)); 68 | } 69 | 70 | if (!authenticationEvent.Created.HasValue) 71 | { 72 | throw new ArgumentNullException(nameof(AuthenticationEvent.Created)); 73 | } 74 | 75 | try 76 | { 77 | await using NpgsqlCommand pgcom = _dataSource.CreateCommand(INSERTAUTHNEVENT); 78 | 79 | pgcom.Parameters.AddWithValue("sessionid", NpgsqlTypes.NpgsqlDbType.Text, string.IsNullOrEmpty(authenticationEvent.SessionId) ? DBNull.Value : authenticationEvent.SessionId); 80 | pgcom.Parameters.AddWithValue("externalsessionid", NpgsqlTypes.NpgsqlDbType.Text, string.IsNullOrEmpty(authenticationEvent.ExternalSessionId) ? DBNull.Value : authenticationEvent.ExternalSessionId); 81 | pgcom.Parameters.AddWithValue("subscriptionkey", NpgsqlTypes.NpgsqlDbType.Text, string.IsNullOrEmpty(authenticationEvent.SubscriptionKey) ? DBNull.Value : authenticationEvent.SubscriptionKey); 82 | pgcom.Parameters.AddWithValue("externaltokenissuer", NpgsqlTypes.NpgsqlDbType.Text, string.IsNullOrEmpty(authenticationEvent.ExternalTokenIssuer) ? DBNull.Value : authenticationEvent.ExternalTokenIssuer); 83 | pgcom.Parameters.AddWithValue("created", NpgsqlTypes.NpgsqlDbType.TimestampTz, authenticationEvent.Created.Value.ToOffset(TimeSpan.Zero)); 84 | pgcom.Parameters.AddWithValue("userid", NpgsqlTypes.NpgsqlDbType.Integer, (authenticationEvent.UserId == null) ? DBNull.Value : authenticationEvent.UserId); 85 | pgcom.Parameters.AddWithValue("supplierid", NpgsqlTypes.NpgsqlDbType.Text, string.IsNullOrEmpty(authenticationEvent.SupplierId) ? DBNull.Value : authenticationEvent.SupplierId); 86 | pgcom.Parameters.AddWithValue("orgnumber", NpgsqlTypes.NpgsqlDbType.Integer, (authenticationEvent.OrgNumber == null) ? DBNull.Value : authenticationEvent.OrgNumber); 87 | pgcom.Parameters.AddWithValue("eventtypeid", NpgsqlTypes.NpgsqlDbType.Integer, Convert.ToInt32(authenticationEvent.EventType)); 88 | pgcom.Parameters.AddWithValue("authenticationmethodid", NpgsqlTypes.NpgsqlDbType.Integer, (authenticationEvent.AuthenticationMethod == null) ? DBNull.Value : Convert.ToInt32(authenticationEvent.AuthenticationMethod)); 89 | pgcom.Parameters.AddWithValue("authenticationlevelid", NpgsqlTypes.NpgsqlDbType.Integer, (authenticationEvent.AuthenticationLevel == null) ? DBNull.Value : Convert.ToInt32(authenticationEvent.AuthenticationLevel)); 90 | pgcom.Parameters.AddWithValue("ipaddress", NpgsqlTypes.NpgsqlDbType.Text, string.IsNullOrEmpty(authenticationEvent.IpAddress) ? DBNull.Value : authenticationEvent.IpAddress); 91 | pgcom.Parameters.AddWithValue("isauthenticated", NpgsqlTypes.NpgsqlDbType.Boolean, authenticationEvent.IsAuthenticated); 92 | 93 | 94 | await pgcom.ExecuteNonQueryAsync(); 95 | } 96 | catch (Exception e) 97 | { 98 | _logger.LogError(e, "AuditLog // AuditLogMetadataRepository // InsertAuthenticationEvent // Exception"); 99 | throw; 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Extensions/AuditLogDependencyInjectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Repositories.Interfaces; 2 | using Altinn.Auth.AuditLog.Persistence; 3 | using Altinn.Auth.AuditLog.Persistence.Configuration; 4 | using Altinn.Authorization.ServiceDefaults.Npgsql.Yuniql; 5 | using Microsoft.Extensions.DependencyInjection.Extensions; 6 | using Microsoft.Extensions.FileProviders; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.Extensions.Options; 10 | using Npgsql; 11 | 12 | namespace Microsoft.Extensions.DependencyInjection; 13 | 14 | /// 15 | /// Extension methods for adding resource registry services to the dependency injection container. 16 | /// 17 | public static class AuditLogDependencyInjectionExtensions 18 | { 19 | /// 20 | /// Registers auditlog persistence services with the dependency injection container of a host application. 21 | /// 22 | /// The . 23 | /// for further chaining. 24 | public static IHostApplicationBuilder AddAuditLogPersistence( 25 | this IHostApplicationBuilder builder) 26 | { 27 | builder.AddAuthenticationEventRepository(); 28 | builder.AddauthorizationEventRepository(); 29 | builder.AddPartitionManagementRepository(); 30 | 31 | return builder; 32 | } 33 | 34 | /// 35 | /// Registers a with the dependency injection container of a host application. 36 | /// 37 | /// The . 38 | /// for further chaining. 39 | public static IHostApplicationBuilder AddAuthenticationEventRepository( 40 | this IHostApplicationBuilder builder) 41 | { 42 | builder.AddDatabase(); 43 | 44 | builder.Services.TryAddTransient(); 45 | 46 | return builder; 47 | } 48 | 49 | /// 50 | /// Registers a with the dependency injection container of a host application. 51 | /// 52 | /// The . 53 | /// for further chaining. 54 | public static IHostApplicationBuilder AddauthorizationEventRepository( 55 | this IHostApplicationBuilder builder) 56 | { 57 | builder.AddDatabase(); 58 | builder.Services.TryAddSingleton(TimeProvider.System); 59 | 60 | builder.Services.TryAddTransient(); 61 | 62 | return builder; 63 | } 64 | 65 | /// 66 | /// Registers a with the dependency injection container of a host application. 67 | /// 68 | /// The . 69 | /// for further chaining. 70 | public static IHostApplicationBuilder AddPartitionManagementRepository( 71 | this IHostApplicationBuilder builder) 72 | { 73 | builder.AddAdminDatabase(); 74 | builder.Services.TryAddSingleton(); 75 | 76 | return builder; 77 | } 78 | 79 | private static IHostApplicationBuilder AddDatabase(this IHostApplicationBuilder builder) 80 | { 81 | if (builder.Services.Contains(Marker.ServiceDescriptor)) 82 | { 83 | // already added 84 | return builder; 85 | } 86 | 87 | builder.Services.Add(Marker.ServiceDescriptor); 88 | 89 | var fs = new ManifestEmbeddedFileProvider(typeof(AuditLogDependencyInjectionExtensions).Assembly, "Migration"); 90 | string? migrationConnectionString = null; 91 | 92 | builder.AddAltinnPostgresDataSource(cfg => migrationConnectionString = cfg.Migrate.ConnectionString) 93 | .AddYuniqlMigrations(typeof(Marker), cfg => 94 | { 95 | cfg.WorkspaceFileProvider = fs; 96 | cfg.Workspace = "/"; 97 | }); 98 | 99 | builder.Services.AddSingleton(new AdminDbSettings 100 | { 101 | ConnectionString = migrationConnectionString!, 102 | }); 103 | 104 | return builder; 105 | } 106 | 107 | private static IHostApplicationBuilder AddAdminDatabase(this IHostApplicationBuilder builder) 108 | { 109 | builder.AddDatabase(); 110 | 111 | builder.Services.AddKeyedSingleton( 112 | serviceKey: typeof(IPartitionManagerRepository), 113 | (sp, _) => 114 | { 115 | var options = sp.GetRequiredService(); 116 | var connectionStringBuilder = new NpgsqlConnectionStringBuilder(options.ConnectionString); 117 | connectionStringBuilder.Pooling = false; 118 | 119 | var builder = new NpgsqlDataSourceBuilder(connectionStringBuilder.ConnectionString); 120 | builder.UseLoggerFactory(sp.GetRequiredService()); 121 | return builder.BuildMultiHost(); 122 | }); 123 | 124 | return builder; 125 | } 126 | 127 | private sealed class Marker 128 | { 129 | public static readonly ServiceDescriptor ServiceDescriptor = ServiceDescriptor.Singleton(); 130 | } 131 | 132 | private sealed class AdminDbSettings 133 | { 134 | public required string ConnectionString { get; init; } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Functions.Tests/Clients/AuditLogClientTest.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Enum; 2 | using Altinn.Auth.AuditLog.Core.Models; 3 | using Altinn.Auth.AuditLog.Functions.Clients; 4 | using Altinn.Auth.AuditLog.Functions.Clients.Interfaces; 5 | using Altinn.Auth.AuditLog.Functions.Configuration; 6 | using Altinn.Auth.AuditLog.Functions.Tests.Helpers; 7 | using Azure.Messaging; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.Extensions.Logging.Abstractions; 10 | using Microsoft.Extensions.Options; 11 | using Moq; 12 | using Moq.Protected; 13 | using System; 14 | using System.Buffers; 15 | using System.Collections.Generic; 16 | using System.Linq; 17 | using System.Net; 18 | using System.Text; 19 | using System.Threading.Tasks; 20 | 21 | namespace Altinn.Auth.AuditLog.Functions.Tests.Clients 22 | { 23 | public class AuditLogClientTest 24 | { 25 | IOptions _platformSettings = Options.Create(new PlatformSettings 26 | { 27 | AuditLogApiEndpoint = "https://platform.test.altinn.cloud/" 28 | }); 29 | 30 | private readonly AuthenticationEvent authenticationEvent = new AuthenticationEvent() 31 | { 32 | UserId = 20000003, 33 | Created = DateTime.UtcNow, 34 | AuthenticationMethod = AuthenticationMethod.BankID, 35 | EventType = AuthenticationEventType.Authenticate, 36 | AuthenticationLevel = SecurityLevel.VerySensitive, 37 | }; 38 | 39 | /// 40 | /// Verify that the endpoint the client sends a request to is set correctly 41 | /// 42 | [Fact] 43 | public async Task SaveAuthenticationEvent_SuccessResponse() 44 | { 45 | // Arrange 46 | var handlerMock = CreateMessageHandlerMock( 47 | "https://platform.test.altinn.cloud/auditlog/api/v1/authenticationevent", 48 | HttpStatusCode.OK); 49 | 50 | var client = new AuditLogClient(new NullLogger(), new HttpClient(handlerMock.Object), _platformSettings); 51 | // Act 52 | await client.SaveAuthenticationEvent(authenticationEvent, CancellationToken.None); 53 | 54 | // Assert 55 | handlerMock.VerifyAll(); 56 | } 57 | 58 | [Fact] 59 | public async Task SaveAuthenticationEvent_NonSuccessResponse_ErrorLoggedAndExceptionThrown() 60 | { 61 | // Arrange 62 | var handlerMock = CreateMessageHandlerMock( 63 | "https://platform.test.altinn.cloud/auditlog/api/v1/authenticationevent", 64 | HttpStatusCode.ServiceUnavailable); 65 | 66 | var client = CreateTestInstance(handlerMock.Object); 67 | 68 | // Act 69 | 70 | await Assert.ThrowsAsync(async () => await client.SaveAuthenticationEvent(authenticationEvent, CancellationToken.None)); 71 | 72 | // Assert 73 | handlerMock.VerifyAll(); 74 | } 75 | 76 | /// 77 | /// Verify that the endpoint the client sends a request to is set correctly 78 | /// 79 | [Fact] 80 | public async Task SaveAuthorizationEvent_SuccessResponse() 81 | { 82 | // Arrange 83 | var handlerMock = CreateMessageHandlerMock( 84 | "https://platform.test.altinn.cloud/auditlog/api/v1/authorizationevent", 85 | HttpStatusCode.OK); 86 | 87 | var client = new AuditLogClient(new NullLogger(), new HttpClient(handlerMock.Object), _platformSettings); 88 | // Act 89 | await client.SaveAuthorizationEvent(new ReadOnlySequence(TestDataHelper.GetAuthorizationEvent_JsonData()), CancellationToken.None); 90 | 91 | // Assert 92 | handlerMock.VerifyAll(); 93 | } 94 | 95 | [Fact] 96 | public async Task SaveAuthorizationEvent_NonSuccessResponse_ErrorLoggedAndExceptionThrown() 97 | { 98 | // Arrange 99 | var handlerMock = CreateMessageHandlerMock( 100 | "https://platform.test.altinn.cloud/auditlog/api/v1/authorizationevent", 101 | HttpStatusCode.ServiceUnavailable); 102 | 103 | var client = CreateTestInstance(handlerMock.Object); 104 | 105 | // Act 106 | 107 | await Assert.ThrowsAsync(async () => await client.SaveAuthorizationEvent(new ReadOnlySequence(TestDataHelper.GetAuthorizationEvent_JsonData()), CancellationToken.None)); 108 | 109 | // Assert 110 | handlerMock.VerifyAll(); 111 | } 112 | 113 | private static Mock CreateMessageHandlerMock(string clientEndpoint, HttpStatusCode statusCode) 114 | { 115 | var messageHandlerMock = new Mock(MockBehavior.Strict); 116 | 117 | messageHandlerMock.Protected() 118 | .Setup>("SendAsync", ItExpr.Is(rm => rm.RequestUri.Equals(clientEndpoint)), ItExpr.IsAny()) 119 | .ReturnsAsync((HttpRequestMessage request, CancellationToken token) => 120 | { 121 | var response = new HttpResponseMessage(statusCode); 122 | return response; 123 | }) 124 | .Verifiable(); 125 | 126 | return messageHandlerMock; 127 | } 128 | 129 | private AuditLogClient CreateTestInstance(HttpMessageHandler messageHandlerMock) 130 | { 131 | return new AuditLogClient( 132 | new NullLogger(), 133 | new HttpClient(messageHandlerMock), 134 | _platformSettings); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/AuthorizationEventRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Text.Json; 3 | using Altinn.Auth.AuditLog.Core.Models; 4 | using Altinn.Auth.AuditLog.Core.Repositories.Interfaces; 5 | using Microsoft.Extensions.Logging; 6 | using Npgsql; 7 | 8 | namespace Altinn.Auth.AuditLog.Persistence 9 | { 10 | [ExcludeFromCodeCoverage] 11 | public class AuthorizationEventRepository : IAuthorizationEventRepository 12 | { 13 | private readonly ILogger _logger; 14 | private readonly NpgsqlDataSource _dataSource; 15 | 16 | /// 17 | /// Initializes a new instance of the class 18 | /// 19 | /// The postgreSQL datasource for AuditLogDB 20 | /// handler for logger service 21 | public AuthorizationEventRepository( 22 | NpgsqlDataSource dataSource, 23 | ILogger logger) 24 | { 25 | _dataSource = dataSource; 26 | _logger = logger; 27 | } 28 | 29 | public async Task InsertAuthorizationEvent(AuthorizationEvent authorizationEvent) 30 | { 31 | const string INSERTAUTHZEVENT = /*strpsql*/ 32 | """ 33 | INSERT INTO authz.eventlogv1( 34 | sessionid, 35 | created, 36 | subjectuserid, 37 | subjectorgcode, 38 | subjectorgnumber, 39 | subjectparty, 40 | resourcepartyid, 41 | resource, 42 | instanceid, 43 | operation, 44 | ipaddress, 45 | contextrequestjson, 46 | decision, 47 | subject_party_uuid 48 | ) 49 | VALUES ( 50 | @sessionid, 51 | @created, 52 | @subjectuserid, 53 | @subjectorgcode, 54 | @subjectorgnumber, 55 | @subjectparty, 56 | @resourcepartyid, 57 | @resource, 58 | @instanceid, 59 | @operation, 60 | @ipaddress, 61 | @contextrequestjson, 62 | @decision, 63 | @subjectpartyuuid 64 | ) 65 | RETURNING *; 66 | """; 67 | 68 | if (authorizationEvent == null) 69 | { 70 | throw new ArgumentNullException(nameof(authorizationEvent)); 71 | } 72 | 73 | if (!authorizationEvent.Created.HasValue) 74 | { 75 | throw new ArgumentNullException(nameof(authorizationEvent), "Created must not be null"); 76 | } 77 | 78 | if (string.IsNullOrEmpty(authorizationEvent.Operation)) 79 | { 80 | throw new ArgumentNullException(nameof(authorizationEvent), "Operation must not be null or empty"); 81 | } 82 | 83 | if (authorizationEvent.ContextRequestJson.ValueKind != JsonValueKind.Object) 84 | { 85 | throw new ArgumentNullException(nameof(authorizationEvent), "Context request must be an object"); 86 | } 87 | 88 | try 89 | { 90 | await using NpgsqlCommand pgcom = _dataSource.CreateCommand(INSERTAUTHZEVENT); 91 | pgcom.Parameters.AddWithValue("sessionid", NpgsqlTypes.NpgsqlDbType.Text, string.IsNullOrEmpty(authorizationEvent.SessionId) ? DBNull.Value : authorizationEvent.SessionId); 92 | pgcom.Parameters.AddWithValue("created", NpgsqlTypes.NpgsqlDbType.TimestampTz, authorizationEvent.Created.Value.ToOffset(TimeSpan.Zero)); 93 | pgcom.Parameters.AddWithValue("subjectuserid", NpgsqlTypes.NpgsqlDbType.Integer, (authorizationEvent.SubjectUserId == null) ? DBNull.Value : authorizationEvent.SubjectUserId); 94 | pgcom.Parameters.AddWithValue("subjectorgcode", NpgsqlTypes.NpgsqlDbType.Text, string.IsNullOrEmpty(authorizationEvent.SubjectOrgCode) ? DBNull.Value : authorizationEvent.SubjectOrgCode); 95 | pgcom.Parameters.AddWithValue("subjectorgnumber", NpgsqlTypes.NpgsqlDbType.Integer, (authorizationEvent.SubjectOrgNumber == null) ? DBNull.Value : authorizationEvent.SubjectOrgNumber); 96 | pgcom.Parameters.AddWithValue("subjectparty", NpgsqlTypes.NpgsqlDbType.Integer, (authorizationEvent.SubjectParty == null) ? DBNull.Value : authorizationEvent.SubjectParty); 97 | pgcom.Parameters.AddWithValue("resourcepartyid", NpgsqlTypes.NpgsqlDbType.Integer, (authorizationEvent.ResourcePartyId == null) ? DBNull.Value : authorizationEvent.ResourcePartyId); 98 | pgcom.Parameters.AddWithValue("resource", NpgsqlTypes.NpgsqlDbType.Text, string.IsNullOrEmpty(authorizationEvent.Resource) ? DBNull.Value : authorizationEvent.Resource); 99 | pgcom.Parameters.AddWithValue("instanceid", NpgsqlTypes.NpgsqlDbType.Text, string.IsNullOrEmpty(authorizationEvent.InstanceId) ? DBNull.Value : authorizationEvent.InstanceId); 100 | pgcom.Parameters.AddWithValue("operation", NpgsqlTypes.NpgsqlDbType.Text, string.IsNullOrEmpty(authorizationEvent.Operation) ? DBNull.Value : authorizationEvent.Operation); 101 | pgcom.Parameters.AddWithValue("ipaddress", NpgsqlTypes.NpgsqlDbType.Text, string.IsNullOrEmpty(authorizationEvent.IpAdress) ? DBNull.Value : authorizationEvent.IpAdress); 102 | pgcom.Parameters.AddWithValue("contextrequestjson", NpgsqlTypes.NpgsqlDbType.Jsonb, authorizationEvent.ContextRequestJson); 103 | pgcom.Parameters.AddWithValue("decision", NpgsqlTypes.NpgsqlDbType.Integer, Convert.ToInt32(authorizationEvent.Decision)); 104 | pgcom.Parameters.AddWithValue("subjectpartyuuid", NpgsqlTypes.NpgsqlDbType.Text, string.IsNullOrEmpty(authorizationEvent.SubjectPartyUuid) ? DBNull.Value : authorizationEvent.SubjectPartyUuid); 105 | 106 | await pgcom.ExecuteNonQueryAsync(); 107 | } 108 | catch (Exception e) 109 | { 110 | _logger.LogError(e, "AuditLog // AuditLogMetadataRepository // InsertAuthorizationEvent // Exception"); 111 | throw; 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog/Services/PartitionCreationHostedService.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Core.Models; 2 | using Altinn.Auth.AuditLog.Core.Repositories.Interfaces; 3 | using Altinn.Auth.AuditLog.Core.Services.Interfaces; 4 | using Npgsql; 5 | using System.Diagnostics; 6 | 7 | namespace Altinn.Auth.AuditLog.Services 8 | { 9 | public class PartitionCreationHostedService : IHostedService, IDisposable 10 | { 11 | private readonly object _lock = new(); 12 | 13 | private readonly ILogger _logger; 14 | private readonly IPartitionManagerRepository _partitionManagerRepository; 15 | private readonly TimeProvider _timeProvider; 16 | private ITimer? _timer; 17 | private CancellationTokenSource? _stoppingCts; 18 | private bool _disposed; 19 | 20 | private Task _runningJob = Task.CompletedTask; 21 | 22 | /// 23 | /// For testing purposes. 24 | /// 25 | internal Task RunningJob => Volatile.Read(ref _runningJob); 26 | 27 | 28 | public PartitionCreationHostedService( 29 | ILogger logger, 30 | IPartitionManagerRepository partitionManagerRepository, 31 | TimeProvider timeProvider) 32 | { 33 | _logger = logger; 34 | _partitionManagerRepository = partitionManagerRepository; 35 | _timeProvider = timeProvider; 36 | } 37 | 38 | public Task StartAsync(CancellationToken cancellationToken) 39 | { 40 | // Create linked token to allow cancelling executing task from provided token 41 | _stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); 42 | 43 | _timer = _timeProvider.CreateTimer( 44 | static (state) => 45 | { 46 | var self = (PartitionCreationHostedService)state!; 47 | self.CreateMonthlyPartitionFromTimer(); 48 | }, 49 | dueTime: TimeSpan.FromDays(1), 50 | period: TimeSpan.FromDays(1), 51 | state: this 52 | ); 53 | 54 | // if it does not run at once 55 | return CreateMonthlyPartition(cancellationToken); 56 | } 57 | 58 | public async Task StopAsync(CancellationToken cancellationToken) 59 | { 60 | lock (_lock) 61 | { 62 | if (_disposed) return; 63 | } 64 | 65 | if (_timer is { } timer) 66 | { 67 | await timer.DisposeAsync(); 68 | } 69 | 70 | if (_stoppingCts is { } cts) 71 | { 72 | await cts.CancelAsync(); 73 | } 74 | } 75 | 76 | public void Dispose() 77 | { 78 | lock (_lock) 79 | { 80 | if (_disposed) return; 81 | _disposed = true; 82 | } 83 | 84 | _timer?.Dispose(); 85 | _stoppingCts?.Dispose(); 86 | } 87 | 88 | private void CreateMonthlyPartitionFromTimer() 89 | { 90 | Task newJob = null!; 91 | newJob = Task.Run(async () => 92 | { 93 | var token = _stoppingCts!.Token; 94 | 95 | try 96 | { 97 | await CreateMonthlyPartition(token); 98 | } 99 | catch (Exception ex) 100 | { 101 | _logger.LogError($"Error while creating partitions {ex}"); 102 | } 103 | 104 | lock (_lock) 105 | { 106 | if (Volatile.Read(ref _runningJob) == newJob) 107 | { 108 | Volatile.Write(ref _runningJob, Task.CompletedTask); 109 | } 110 | } 111 | }); 112 | 113 | lock (_lock) 114 | { 115 | var runningJob = Volatile.Read(ref _runningJob); 116 | if (!runningJob.IsCompleted) 117 | { 118 | newJob = Task.WhenAll(newJob, runningJob); 119 | } 120 | 121 | Volatile.Write(ref _runningJob, newJob); 122 | } 123 | } 124 | 125 | private async Task CreateMonthlyPartition(CancellationToken cancellationToken) 126 | { 127 | var partitions = GetPartitionsForCurrentAndAdjacentMonths(); 128 | 129 | await _partitionManagerRepository.CreatePartitions(partitions, cancellationToken); 130 | } 131 | 132 | internal IReadOnlyList GetPartitionsForCurrentAndAdjacentMonths() 133 | { 134 | string authenticationSchemaName = "authentication"; 135 | string authzSchemaName = "authz"; 136 | 137 | // Get current dateonly 138 | var now = DateOnly.FromDateTime(_timeProvider.GetUtcNow().UtcDateTime); 139 | 140 | // Define the date ranges for the past, current and next month partitions 141 | var (currentMonthStartDate, currentMonthEndDate) = GetMonthStartAndEndDate(now); 142 | var (pastMonthStartDate, pastMonthEndDate) = GetMonthStartAndEndDate(now.AddMonths(-1)); 143 | var (nextMonthStartDate, nextMonthEndDate) = GetMonthStartAndEndDate(now.AddMonths(1)); 144 | 145 | // Create partition names 146 | var pastMonthPartitionName = $"eventlogv1_y{pastMonthStartDate.Year}m{pastMonthStartDate.Month:D2}"; 147 | var currentMonthPartitionName = $"eventlogv1_y{currentMonthStartDate.Year}m{currentMonthStartDate.Month:D2}"; 148 | var nextMonthPartitionName = $"eventlogv1_y{nextMonthStartDate.Year}m{nextMonthStartDate.Month:D2}"; 149 | 150 | // List of partitions for both schemas 151 | return new List 152 | { 153 | new Partition { SchemaName = authenticationSchemaName, Name = pastMonthPartitionName, StartDate = pastMonthStartDate, EndDate = pastMonthEndDate }, 154 | new Partition { SchemaName = authenticationSchemaName, Name = currentMonthPartitionName, StartDate = currentMonthStartDate, EndDate = currentMonthEndDate }, 155 | new Partition { SchemaName = authenticationSchemaName, Name = nextMonthPartitionName, StartDate = nextMonthStartDate, EndDate = nextMonthEndDate }, 156 | new Partition { SchemaName = authzSchemaName, Name = pastMonthPartitionName, StartDate = pastMonthStartDate, EndDate = pastMonthEndDate }, 157 | new Partition { SchemaName = authzSchemaName, Name = currentMonthPartitionName, StartDate = currentMonthStartDate, EndDate = currentMonthEndDate }, 158 | new Partition { SchemaName = authzSchemaName, Name = nextMonthPartitionName, StartDate = nextMonthStartDate, EndDate = nextMonthEndDate } 159 | }; 160 | } 161 | 162 | internal (DateOnly startDate, DateOnly endDate) GetMonthStartAndEndDate(DateOnly date) 163 | { 164 | DateOnly startDate = new DateOnly(date.Year, date.Month, 1); 165 | DateOnly endDate = startDate.AddMonths(1); 166 | return (startDate, endDate); 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Altinn.Auth.AuditLog.Persistence/Migration/v0.00/03-setup-tables.sql: -------------------------------------------------------------------------------- 1 | -- Table: authentication.eventtype 2 | CREATE TABLE IF NOT EXISTS authentication.authenticationeventtype 3 | ( 4 | authenticationeventtypeid integer PRIMARY KEY, 5 | name text, 6 | description text 7 | ) 8 | TABLESPACE pg_default; 9 | 10 | GRANT ALL ON TABLE authentication.authenticationeventtype TO "${APP-USER}"; 11 | 12 | GRANT ALL ON TABLE authentication.authenticationeventtype TO "${YUNIQL-USER}"; 13 | 14 | INSERT INTO authentication.authenticationeventtype( 15 | authenticationeventtypeid, name, description) 16 | VALUES (1, 'Authenticate', 'Authenticate'); 17 | INSERT INTO authentication.authenticationeventtype( 18 | authenticationeventtypeid, name, description) 19 | VALUES (2, 'Refresh', 'Refresh'); 20 | INSERT INTO authentication.authenticationeventtype( 21 | authenticationeventtypeid, name, description) 22 | VALUES (3, 'TokenExchange', 'TokenExchange'); 23 | INSERT INTO authentication.authenticationeventtype( 24 | authenticationeventtypeid, name, description) 25 | VALUES (4, 'Logout', 'Logout'); 26 | 27 | -- Table: authentication.authenticationmethod 28 | CREATE TABLE IF NOT EXISTS authentication.authenticationmethod 29 | ( 30 | authenticationmethodid integer PRIMARY KEY, 31 | name text, 32 | description text 33 | ) 34 | TABLESPACE pg_default; 35 | 36 | GRANT ALL ON TABLE authentication.authenticationmethod TO "${APP-USER}"; 37 | 38 | GRANT ALL ON TABLE authentication.authenticationmethod TO "${YUNIQL-USER}"; 39 | 40 | INSERT INTO authentication.authenticationmethod( 41 | authenticationmethodid, name, description) 42 | VALUES (-1, 'NotDefined', 'NotDefined'); 43 | INSERT INTO authentication.authenticationmethod( 44 | authenticationmethodid, name, description) 45 | VALUES (0, 'AltinnPIN', 'AltinnPIN'); 46 | INSERT INTO authentication.authenticationmethod( 47 | authenticationmethodid, name, description) 48 | VALUES (1, 'BankID', 'BankID'); 49 | INSERT INTO authentication.authenticationmethod( 50 | authenticationmethodid, name, description) 51 | VALUES (2, 'BuyPass', 'BuyPass'); 52 | INSERT INTO authentication.authenticationmethod( 53 | authenticationmethodid, name, description) 54 | VALUES (3, 'SAML2', 'SAML2'); 55 | INSERT INTO authentication.authenticationmethod( 56 | authenticationmethodid, name, description) 57 | VALUES (4, 'SMSPIN', 'SMSPIN'); 58 | INSERT INTO authentication.authenticationmethod( 59 | authenticationmethodid, name, description) 60 | VALUES (5, 'StaticPassword', 'StaticPassword'); 61 | INSERT INTO authentication.authenticationmethod( 62 | authenticationmethodid, name, description) 63 | VALUES (6, 'TaxPIN', 'TaxPIN'); 64 | INSERT INTO authentication.authenticationmethod( 65 | authenticationmethodid, name, description) 66 | VALUES (7, 'FederationNotUsedAnymore', 'FederationNotUsedAnymore'); 67 | INSERT INTO authentication.authenticationmethod( 68 | authenticationmethodid, name, description) 69 | VALUES (8, 'SelfIdentified', 'SelfIdentified'); 70 | INSERT INTO authentication.authenticationmethod( 71 | authenticationmethodid, name, description) 72 | VALUES (9, 'EnterpriseIdentified', 'EnterpriseIdentified'); 73 | INSERT INTO authentication.authenticationmethod( 74 | authenticationmethodid, name, description) 75 | VALUES (10, 'Commfides', 'Commfides'); 76 | INSERT INTO authentication.authenticationmethod( 77 | authenticationmethodid, name, description) 78 | VALUES (11, 'MinIDPin', 'MinIDPin'); 79 | INSERT INTO authentication.authenticationmethod( 80 | authenticationmethodid, name, description) 81 | VALUES (12, 'OpenSshIdentified', 'OpenSshIdentified'); 82 | INSERT INTO authentication.authenticationmethod( 83 | authenticationmethodid, name, description) 84 | VALUES (13, 'EIDAS', 'EIDAS'); 85 | INSERT INTO authentication.authenticationmethod( 86 | authenticationmethodid, name, description) 87 | VALUES (14, 'BankIDMobil', 'BankIDMobil'); 88 | INSERT INTO authentication.authenticationmethod( 89 | authenticationmethodid, name, description) 90 | VALUES (15, 'MinIDOTC', 'MinIDOTC'); 91 | INSERT INTO authentication.authenticationmethod( 92 | authenticationmethodid, name, description) 93 | VALUES (16, 'EnterpriseMaskinportenIdentified', 'EnterpriseMaskinportenIdentified'); 94 | INSERT INTO authentication.authenticationmethod( 95 | authenticationmethodid, name, description) 96 | VALUES (17, 'IdportenTestId', 'IdportenTestId'); 97 | INSERT INTO authentication.authenticationmethod( 98 | authenticationmethodid, name, description) 99 | VALUES (18, 'MinIDApp', 'MinIDApp'); 100 | INSERT INTO authentication.authenticationmethod( 101 | authenticationmethodid, name, description) 102 | VALUES (19, 'VirksomhetsBruker', 'VirksomhetsBruker'); 103 | INSERT INTO authentication.authenticationmethod( 104 | authenticationmethodid, name, description) 105 | VALUES (20, 'MaskinPorten', 'MaskinPorten'); 106 | 107 | 108 | 109 | -- Table: authentication.authenticationlevel 110 | CREATE TABLE IF NOT EXISTS authentication.authenticationlevel 111 | ( 112 | authenticationlevelid integer PRIMARY KEY, 113 | name text, 114 | description text 115 | ) 116 | TABLESPACE pg_default; 117 | 118 | GRANT ALL ON TABLE authentication.authenticationlevel TO "${APP-USER}"; 119 | 120 | GRANT ALL ON TABLE authentication.authenticationlevel TO "${YUNIQL-USER}"; 121 | 122 | INSERT INTO authentication.authenticationlevel( 123 | authenticationlevelid, name, description) 124 | VALUES (0, 'SelfIdentified','SelfIdentified'); 125 | INSERT INTO authentication.authenticationlevel( 126 | authenticationlevelid, name, description) 127 | VALUES (1, 'NotSensitive','NotSensitive'); 128 | INSERT INTO authentication.authenticationlevel( 129 | authenticationlevelid, name, description) 130 | VALUES (2, 'QuiteSensitive', 'QuiteSensitive'); 131 | INSERT INTO authentication.authenticationlevel( 132 | authenticationlevelid, name, description) 133 | VALUES (3, 'Sensitive', 'Sensitive'); 134 | INSERT INTO authentication.authenticationlevel( 135 | authenticationlevelid, name, description) 136 | VALUES (4, 'VerySensitive', 'VerySensitive'); 137 | 138 | -- Table: authentication.eventlog 139 | CREATE TABLE IF NOT EXISTS authentication.eventlog 140 | ( 141 | sessionid text, 142 | externalsessionid text, 143 | subscriptionkey text, 144 | externaltokenissuer text, 145 | created timestamp with time zone NOT NULL, 146 | userid integer, 147 | supplierid text, 148 | orgnumber integer, 149 | eventtypeid integer, 150 | authenticationmethodid integer, 151 | authenticationlevelid integer, 152 | ipaddress text, 153 | isauthenticated boolean NOT NULL, 154 | CONSTRAINT authenticationeventtype_fkey FOREIGN KEY (eventtypeid) 155 | REFERENCES authentication.authenticationeventtype (authenticationeventtypeid) MATCH SIMPLE 156 | ON UPDATE NO ACTION 157 | ON DELETE NO ACTION 158 | NOT VALID, 159 | CONSTRAINT authenticationlevel_fkey FOREIGN KEY (authenticationlevelid) 160 | REFERENCES authentication.authenticationlevel (authenticationlevelid) MATCH SIMPLE 161 | ON UPDATE NO ACTION 162 | ON DELETE NO ACTION 163 | NOT VALID, 164 | CONSTRAINT authenticationmethod_fkey FOREIGN KEY (authenticationmethodid) 165 | REFERENCES authentication.authenticationmethod (authenticationmethodid) MATCH SIMPLE 166 | ON UPDATE NO ACTION 167 | ON DELETE NO ACTION 168 | NOT VALID 169 | ) 170 | TABLESPACE pg_default; 171 | 172 | GRANT ALL ON TABLE authentication.eventlog TO "${APP-USER}"; 173 | 174 | GRANT ALL ON TABLE authentication.eventlog TO "${YUNIQL-USER}"; 175 | -------------------------------------------------------------------------------- /test/Altinn.Auth.AuditLog.Tests/DbFixture.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Auth.AuditLog.Persistence.Configuration; 2 | using Altinn.Authorization.ServiceDefaults.Npgsql.Yuniql; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Npgsql; 7 | using Testcontainers.PostgreSql; 8 | 9 | namespace Altinn.Auth.AuditLog.Tests; 10 | 11 | public class DbFixture 12 | : IAsyncLifetime 13 | { 14 | private const int MAX_CONCURRENCY = 20; 15 | 16 | Singleton.Ref? _inner; 17 | 18 | public async Task InitializeAsync() 19 | { 20 | _inner = await Singleton.Get(); 21 | } 22 | 23 | public Task CreateDbAsync() 24 | => _inner!.Value.CreateDbAsync(this); 25 | 26 | private Task DropDbAsync(OwnedDb ownedDb) 27 | => _inner!.Value.DropDatabaseAsync(ownedDb); 28 | 29 | public async Task DisposeAsync() 30 | { 31 | if (_inner is { } inner) 32 | { 33 | await inner.DisposeAsync(); 34 | } 35 | } 36 | 37 | private class Inner : IAsyncLifetime 38 | { 39 | private int _dbCounter = 0; 40 | private readonly AsyncLock _dbLock = new(); 41 | private readonly PostgreSqlContainer _dbContainer = new PostgreSqlBuilder() 42 | .WithImage("timescale/timescaledb:2.1.0-pg11") 43 | .WithUsername("test-db-admin") 44 | .WithPassword(Guid.NewGuid().ToString()) 45 | .WithDatabase("authauditlogdb") 46 | .WithCleanUp(true) 47 | .Build(); 48 | 49 | private readonly AsyncConcurrencyLimiter _throtler = new(MAX_CONCURRENCY); 50 | 51 | string? _connectionString; 52 | NpgsqlDataSource? _db; 53 | 54 | public async Task InitializeAsync() 55 | { 56 | await _dbContainer.StartAsync(); 57 | _connectionString = _dbContainer.GetConnectionString() + "; Include Error Detail=true; Pooling=false;"; 58 | _db = NpgsqlDataSource.Create(_connectionString); 59 | } 60 | 61 | public async Task CreateDbAsync(DbFixture fixture) 62 | { 63 | var counter = Interlocked.Increment(ref _dbCounter); 64 | var dbName = $"test_{counter}"; 65 | var appUserName = $"app_{counter}"; 66 | var adminUserName = $"admin_{counter}"; 67 | 68 | var appUserPassword = Guid.NewGuid().ToString(); 69 | var adminUserPassword = Guid.NewGuid().ToString(); 70 | 71 | var ticket = await _throtler.Acquire(); 72 | 73 | try 74 | { 75 | // only create 1 db at once 76 | using var guard = await _dbLock.Acquire(); 77 | 78 | await using var batch = _db!.CreateBatch(); 79 | var cmd = batch.CreateBatchCommand(); 80 | cmd.CommandText = /*strpsql*/$"""CREATE ROLE "{appUserName}" LOGIN PASSWORD '{appUserPassword}'"""; 81 | batch.BatchCommands.Add(cmd); 82 | 83 | cmd = batch.CreateBatchCommand(); 84 | cmd.CommandText = /*strpsql*/$"""CREATE ROLE "{adminUserName}" LOGIN PASSWORD '{adminUserPassword}'"""; 85 | batch.BatchCommands.Add(cmd); 86 | 87 | cmd = batch.CreateBatchCommand(); 88 | cmd.CommandText = /*strpsql*/$"""CREATE DATABASE "{dbName}" OWNER "{adminUserName}" """; 89 | batch.BatchCommands.Add(cmd); 90 | 91 | cmd = batch.CreateBatchCommand(); 92 | cmd.CommandText = /*strpsql*/$"""GRANT CONNECT ON DATABASE "{dbName}" TO "{appUserName}" """; 93 | batch.BatchCommands.Add(cmd); 94 | 95 | await batch.ExecuteNonQueryAsync(); 96 | 97 | var connectionStringBuilder = new NpgsqlConnectionStringBuilder(_connectionString) { Database = dbName, IncludeErrorDetail = true }; 98 | 99 | connectionStringBuilder.Username = adminUserName; 100 | connectionStringBuilder.Password = adminUserPassword; 101 | var adminConnectionString = connectionStringBuilder.ConnectionString; 102 | 103 | connectionStringBuilder.Username = appUserName; 104 | connectionStringBuilder.Password = appUserPassword; 105 | var appConnectionString = connectionStringBuilder.ConnectionString; 106 | 107 | var ownedDb = new OwnedDb(adminConnectionString, appConnectionString, dbName, fixture, ticket); 108 | ticket = null; 109 | return ownedDb; 110 | } 111 | finally 112 | { 113 | ticket?.Dispose(); 114 | } 115 | } 116 | 117 | public async Task DropDatabaseAsync(OwnedDb ownedDb) 118 | { 119 | await using var cmd = _db!.CreateCommand(/*strpsql*/$"DROP DATABASE IF EXISTS {ownedDb.DbName};"); 120 | 121 | await cmd.ExecuteNonQueryAsync(); 122 | } 123 | 124 | public async Task DisposeAsync() 125 | { 126 | if (_db is { }) 127 | { 128 | await _db.DisposeAsync(); 129 | } 130 | 131 | await _dbContainer.DisposeAsync(); 132 | _throtler.Dispose(); 133 | _dbLock.Dispose(); 134 | } 135 | } 136 | 137 | public sealed class OwnedDb : IAsyncDisposable 138 | { 139 | readonly string _adminConnectionString; 140 | readonly string _appConnectionString; 141 | readonly string _dbName; 142 | readonly DbFixture _db; 143 | readonly IDisposable _ticket; 144 | 145 | public OwnedDb(string adminConnectionString, string appConnectionString, string dbName, DbFixture db, IDisposable ticket) 146 | { 147 | _adminConnectionString = adminConnectionString; 148 | _appConnectionString = appConnectionString; 149 | _dbName = dbName; 150 | _db = db; 151 | _ticket = ticket; 152 | } 153 | 154 | public string AdminConnectionString => _adminConnectionString; 155 | 156 | public string AppConnectionString => _appConnectionString; 157 | 158 | internal string DbName => _dbName; 159 | 160 | public void ConfigureApplication(IHostApplicationBuilder builder) 161 | { 162 | var serviceDescriptor = builder.GetAltinnServiceDescriptor(); 163 | ConfigureConfiguration(builder.Configuration, serviceDescriptor.Name); 164 | ConfigureServices(builder.Services, serviceDescriptor.Name); 165 | } 166 | 167 | public void ConfigureConfiguration(IConfigurationBuilder builder, string serviceName) 168 | { 169 | builder.AddInMemoryCollection([ 170 | new($"Altinn:Npgsql:auditlog:ConnectionString", _appConnectionString), 171 | new($"Altinn:Npgsql:auditlog:Migrate:ConnectionString", _adminConnectionString), 172 | ]); 173 | } 174 | 175 | public void ConfigureServices(IServiceCollection services, string serviceName) 176 | { 177 | services.AddOptions() 178 | .Configure(cfg => 179 | { 180 | cfg.Environment = "integrationtest"; 181 | }); 182 | } 183 | 184 | public async ValueTask DisposeAsync() 185 | { 186 | await _db.DropDbAsync(this); 187 | _ticket.Dispose(); 188 | } 189 | } 190 | } 191 | --------------------------------------------------------------------------------