├── 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 |
--------------------------------------------------------------------------------
]