├── .github
├── CODEOWNERS
├── labeler.yml
└── workflows
│ ├── regression-test-AT22.yaml
│ ├── regression-test-AT23.yaml
│ ├── regression-test-AT24.yaml
│ ├── assign-issues-to-projects.yml
│ ├── pr-labeler.yml
│ ├── send-slack-warning.yml
│ ├── container-scan.yml
│ └── codeql-analysis.yml
├── test
├── k6
│ ├── .gitignore
│ ├── src
│ │ ├── reports
│ │ │ └── readme.md
│ │ ├── data
│ │ │ ├── subscriptions
│ │ │ │ ├── 01-app-subscription.json
│ │ │ │ └── 02-generic-subscription.json
│ │ │ ├── events
│ │ │ │ └── 01-event.json
│ │ │ └── app-events
│ │ │ │ └── 01-app-event.json
│ │ ├── errorhandler.js
│ │ ├── api
│ │ │ ├── app-events.js
│ │ │ ├── events.js
│ │ │ └── subscriptions.js
│ │ ├── setup.js
│ │ └── apiHelpers.js
│ ├── docker-compose.yml
│ └── readme.md
├── Postman
│ ├── readme.md
│ ├── Platform Events - localhost.postman_environment.json
│ └── Platform Events - AT22.postman_environment.json
├── Altinn.Platform.Events.Tests
│ ├── ttd-org.pfx
│ ├── Data
│ │ ├── Roles
│ │ │ └── User_1337
│ │ │ │ ├── party_1000
│ │ │ │ └── roles.json
│ │ │ │ ├── party_500700
│ │ │ │ └── roles.json
│ │ │ │ ├── party_1337
│ │ │ │ └── roles.json
│ │ │ │ ├── party_500000
│ │ │ │ └── roles.json
│ │ │ │ ├── party_500001
│ │ │ │ └── roles.json
│ │ │ │ ├── party_500002
│ │ │ │ └── roles.json
│ │ │ │ ├── party_500003
│ │ │ │ └── roles.json
│ │ │ │ └── party_500600
│ │ │ │ └── roles.json
│ │ ├── Profile
│ │ │ └── User
│ │ │ │ ├── 1.json
│ │ │ │ ├── 12345.json
│ │ │ │ ├── 1337.json
│ │ │ │ └── 21023.json
│ │ ├── Register
│ │ │ └── Party
│ │ │ │ ├── 1337.json
│ │ │ │ ├── 1000.json
│ │ │ │ ├── 1001.json
│ │ │ │ ├── 500000.json
│ │ │ │ ├── 1002.json
│ │ │ │ ├── 500001.json
│ │ │ │ ├── 500002.json
│ │ │ │ ├── 500003.json
│ │ │ │ ├── 500600.json
│ │ │ │ └── 500700.json
│ │ ├── PartiesRegisterQueryResponse
│ │ │ ├── oneperson.json
│ │ │ └── twopersons.json
│ │ ├── events
│ │ │ ├── events.json
│ │ │ ├── 3.json
│ │ │ └── 1.json
│ │ └── XacmlJsonResponse
│ │ │ ├── permit_publish_one.json
│ │ │ └── permit_subscribe_one.json
│ ├── platform-org.pfx
│ ├── jwtselfsignedcert.pfx
│ ├── GlobalSuppressions.cs
│ ├── appsettings.unittest.json
│ ├── Utils
│ │ └── TestDataLoader.cs
│ ├── Mocks
│ │ ├── DelegatingHandlerStub.cs
│ │ ├── PublicSigningKeyProviderMock.cs
│ │ ├── Authentication
│ │ │ └── JwtCookiePostConfigureOptionsStub.cs
│ │ └── EventsQueueClientMock.cs
│ ├── Models
│ │ ├── EventsTableEntry.cs
│ │ └── XacmlResourceAttributes.cs
│ ├── ttd-org.pem
│ ├── TestingUtils
│ │ └── XacmlMapperHelperTests.cs
│ ├── platform-org.pem
│ ├── TestingExtensions
│ │ ├── UriExtensionsTests.cs
│ │ └── NpgsqlParameterCollectionExtensionsTests.cs
│ ├── JWTValidationCert.cer
│ └── Constants
│ │ └── XacmlRequestAttribute.cs
└── Altinn.Platform.Events.Functions.Tests
│ ├── GlobalSuppressions.cs
│ ├── IntegrationTests
│ ├── RequiresAzuriteFactAttribute.cs
│ └── TestableRetryBackoffService.cs
│ ├── TestingServices
│ └── platform-org.pfx
│ └── Altinn.Platform.Events.Functions.Tests.csproj
├── src
├── Events
│ ├── Migration
│ │ ├── v0.23
│ │ │ ├── 01-cleanup-obsolete.sql
│ │ │ ├── 02-alter-table.sql
│ │ │ └── 03-setup-function.sql
│ │ ├── _pre
│ │ │ └── README.md
│ │ ├── _erase
│ │ │ └── README.md
│ │ ├── v0.30
│ │ │ └── 01-alter-table.sql
│ │ ├── _post
│ │ │ └── README.md
│ │ ├── v0.12
│ │ │ └── 01-setup-index.sql
│ │ ├── v0.49
│ │ │ ├── 02-functions-and-procedures.sql
│ │ │ └── 01-new-index.sql
│ │ ├── _init
│ │ │ └── README.md
│ │ ├── v0.34
│ │ │ ├── 02-new-indexes.sql
│ │ │ └── 01-create-function.sql
│ │ ├── v0.43
│ │ │ └── 01-new-index.sql
│ │ ├── v0.36
│ │ │ └── 01-new-indexes.sql
│ │ ├── v0.51
│ │ │ └── 01-setup-grants.sql
│ │ ├── v0.39
│ │ │ └── 01-new-indexes.sql
│ │ ├── v0.50
│ │ │ └── 01-setup-new-partition.sql
│ │ ├── v0.46
│ │ │ ├── 03-setup-table-partitions.sql
│ │ │ ├── 02-setup-grants.sql
│ │ │ └── 01-setup-tables.sql
│ │ ├── v0.18
│ │ │ ├── 01-alter-table.sql
│ │ │ └── 02-alter-procedure.sql
│ │ ├── _draft
│ │ │ └── README.md
│ │ ├── v0.27
│ │ │ ├── 01-drop-function.sql
│ │ │ └── 02-alter-function.sql
│ │ ├── v0.44
│ │ │ ├── 02-setup-grants.sql
│ │ │ └── 01-setup-tables.sql
│ │ ├── v0.37
│ │ │ ├── 00-drop-function.sql
│ │ │ └── 01-alter-function.sql
│ │ ├── v0.21
│ │ │ ├── 04-setup-grants.sql
│ │ │ ├── 01-setup-table.sql
│ │ │ ├── 02-setup-index.sql
│ │ │ └── 03-setup-cron-job.sql
│ │ ├── FunctionsAndProcedures
│ │ │ ├── deletesubscription.sql
│ │ │ ├── setvalidsubscription.sql
│ │ │ ├── getsubscription.sql
│ │ │ ├── insertsubscription.sql
│ │ │ ├── getsubscriptionsbyconsumer.sql
│ │ │ ├── getsubscriptions.sql
│ │ │ ├── createlogspartition.sql
│ │ │ ├── getevents.sql
│ │ │ ├── findsubscription.sql
│ │ │ └── getappevents.sql
│ │ ├── v0.00
│ │ │ ├── 02-setup-grants.sql
│ │ │ └── 01-setup-tables.sql
│ │ ├── v0.03
│ │ │ ├── 03-setup-grants.sql
│ │ │ ├── 02-setup-functions.sql
│ │ │ └── 01-setup-tables.sql
│ │ ├── v0.06
│ │ │ └── 01-setup-functions.sql
│ │ ├── v0.29
│ │ │ └── 01-drop-resources.sql
│ │ ├── v0.19
│ │ │ └── 01-setup-cron-job.sql
│ │ ├── v0.04
│ │ │ ├── 01-setup-index.sql
│ │ │ └── 02-setup-functions.sql
│ │ ├── v0.08
│ │ │ └── 01-setup-index.sql
│ │ ├── v0.24
│ │ │ └── 01-cleanup-obsolete.sql
│ │ ├── v0.17
│ │ │ └── 01-setup-functions.sql
│ │ ├── v0.22
│ │ │ └── 01-cleanup-obsolete.sql
│ │ ├── v0.31
│ │ │ └── 01-drop-function.sql
│ │ ├── v0.05
│ │ │ └── 01-setup-functions.sql
│ │ ├── v0.32
│ │ │ └── 01-new-indexes.sql
│ │ ├── v0.09
│ │ │ └── 01-setup-function.sql
│ │ ├── v0.10
│ │ │ └── 01-setup-functions.sql
│ │ ├── v0.01
│ │ │ └── 01-setup-functions.sql
│ │ ├── v0.14
│ │ │ └── 01-setup-functions.sql
│ │ ├── v0.02
│ │ │ └── 01-setup-functions.sql
│ │ ├── v0.11
│ │ │ └── 01-setup-functions.sql
│ │ ├── v0.25
│ │ │ └── 01-alter-function.sql
│ │ ├── ReadMe.txt
│ │ ├── v0.26
│ │ │ └── 01-alter-function.sql
│ │ ├── v0.20
│ │ │ └── 01-alter-function.sql
│ │ ├── v0.13
│ │ │ └── 01-setup-functions.sql
│ │ ├── v0.35
│ │ │ └── 01-alter-function.sql
│ │ ├── v0.38
│ │ │ └── 01-create-function.sql
│ │ ├── v0.33
│ │ │ └── 01-alter-function.sql
│ │ ├── v0.07
│ │ │ └── 01-setup-functions.sql
│ │ └── v0.40
│ │ │ └── 01-create-function.sql
│ ├── appsettings.Development.json
│ ├── appsettings.Production.json
│ ├── Services
│ │ ├── PartiesRegisterQueryRequest.cs
│ │ ├── Interfaces
│ │ │ ├── IClaimsPrincipalProvider.cs
│ │ │ ├── IAppSubscriptionService.cs
│ │ │ ├── IGenericSubscriptionService.cs
│ │ │ ├── IOutboundService.cs
│ │ │ ├── IRegisterService.cs
│ │ │ ├── ISubscriptionService.cs
│ │ │ └── ITraceLogService.cs
│ │ ├── ClaimsPrincipalProvider.cs
│ │ └── PartiesRegisterQueryResponse.cs
│ ├── Repository
│ │ ├── ITraceLogRepository.cs
│ │ └── ICloudEventRepository.cs
│ ├── Mappers
│ │ └── SubscriptionMapper.cs
│ ├── Configuration
│ │ ├── PostgreSQLSettings.cs
│ │ ├── GeneralSettings.cs
│ │ ├── PlatformSettings.cs
│ │ └── QueueStorageSettings.cs
│ ├── Models
│ │ ├── QueuePostReceipt.cs
│ │ ├── SubscriptionList.cs
│ │ ├── ServiceError.cs
│ │ ├── TraceLogActivity.cs
│ │ ├── SubscriptionRequestModel.cs
│ │ ├── LogEntryDto.cs
│ │ ├── TraceLog.cs
│ │ └── CloudEventEnvelope.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Controllers
│ │ ├── WebhookReceiverController.cs
│ │ ├── ErrorController.cs
│ │ └── LogsController.cs
│ ├── Health
│ │ └── HealthCheck.cs
│ ├── Middleware
│ │ └── EnableRequestBodyBufferingMiddleware.cs
│ ├── Extensions
│ │ ├── UriExtensions.cs
│ │ ├── NpgsqlParameterCollectionExtensions.cs
│ │ ├── HttpRequestExtension.cs
│ │ └── CloudEventExtensions.cs
│ ├── Authorization
│ │ └── PublishScopeOrAccessTokenRequirement.cs
│ ├── appsettings.json
│ ├── Exceptions
│ │ └── PlatformHttpException.cs
│ └── Clients
│ │ └── Interfaces
│ │ └── IEventsQueueClient.cs
├── Events.Functions
│ ├── Properties
│ │ ├── serviceDependencies.json
│ │ └── serviceDependencies.local.json
│ ├── Constants
│ │ └── EventConstants.cs
│ ├── Configuration
│ │ ├── PlatformSettings.cs
│ │ ├── CertificateResolverSettings.cs
│ │ ├── EventsOutboundSettings.cs
│ │ └── KeyVaultSettings.cs
│ ├── Services
│ │ ├── Interfaces
│ │ │ ├── IWebhookService.cs
│ │ │ ├── ICertificateResolverService.cs
│ │ │ ├── IRetryBackoffService.cs
│ │ │ └── IKeyVaultService.cs
│ │ └── KeyVaultService.cs
│ ├── host.json
│ ├── local.settings.json
│ ├── TelemetryInitializer.cs
│ ├── EventsInbound.cs
│ ├── Models
│ │ ├── Payloads
│ │ │ └── SlackEnvelope.cs
│ │ └── LogEntryDto.cs
│ ├── Extensions
│ │ └── CloudEventExtensions.cs
│ ├── Queues
│ │ └── QueueSendDelegates.cs
│ ├── Clients
│ │ └── Interfaces
│ │ │ └── IEventsClient.cs
│ └── Program.cs
├── DbTools
│ └── DbTools.csproj
└── Events.Common
│ ├── Altinn.Platform.Events.Common.csproj
│ └── Models
│ └── RetryableEventWrapper.cs
├── .dockerignore
├── container-scan.md
├── stylecop.json
├── docker-compose.yml
├── renovate.json
├── LICENSE.md
├── .gitattributes
└── Dockerfile
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | /.github/CODEOWNERS @altinn/team-core
2 |
--------------------------------------------------------------------------------
/test/k6/.gitignore:
--------------------------------------------------------------------------------
1 | #Junit reports
2 | **/reports/*.xml
3 |
4 |
--------------------------------------------------------------------------------
/test/k6/src/reports/readme.md:
--------------------------------------------------------------------------------
1 | Empty file to ensure folder exists
--------------------------------------------------------------------------------
/test/Postman/readme.md:
--------------------------------------------------------------------------------
1 | ## Collection of all postman related collections and tests
--------------------------------------------------------------------------------
/src/Events/Migration/v0.23/01-cleanup-obsolete.sql:
--------------------------------------------------------------------------------
1 | DROP FUNCTION events.getsubscriptionsexcludeorgs;
2 |
--------------------------------------------------------------------------------
/src/Events/Migration/_pre/README.md:
--------------------------------------------------------------------------------
1 | # The `_pre` directory
2 | Pre migration scripts. Executed every time before any version.
3 |
--------------------------------------------------------------------------------
/src/Events/Migration/_erase/README.md:
--------------------------------------------------------------------------------
1 | # The `_erase` directory
2 | Database cleanup scripts. Executed once only when you do `yuniql erase`.
--------------------------------------------------------------------------------
/src/Events/Migration/v0.30/01-alter-table.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE events.subscription ADD COLUMN IF NOT EXISTS resourcefilter character varying;
--------------------------------------------------------------------------------
/src/Events/Migration/_post/README.md:
--------------------------------------------------------------------------------
1 | # The `_post` directory
2 | Post migration scripts. Executed every time and always the last batch to run.
--------------------------------------------------------------------------------
/src/Events/Migration/v0.12/01-setup-index.sql:
--------------------------------------------------------------------------------
1 | CREATE INDEX IF NOT EXISTS idx_events_id ON events.events USING btree (id ASC NULLS LAST);
2 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.49/02-functions-and-procedures.sql:
--------------------------------------------------------------------------------
1 | -- This script is autogenerated from the tool DbTools. Do not edit manually.
2 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/ttd-org.pfx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/altinn/altinn-events/main/test/Altinn.Platform.Events.Tests/ttd-org.pfx
--------------------------------------------------------------------------------
/src/Events/Migration/_init/README.md:
--------------------------------------------------------------------------------
1 | # The `_init` directory
2 | Initialization scripts. Executed once. This is called the first time you do `yuniql run`.
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Roles/User_1337/party_1000/roles.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "Type": "altinn",
4 | "value": "LOPER"
5 | }
6 | ]
7 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/platform-org.pfx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/altinn/altinn-events/main/test/Altinn.Platform.Events.Tests/platform-org.pfx
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .dockerignore
2 | .env
3 | .git
4 | .gitignore
5 | .vs
6 | .vscode
7 | docker-compose.yml
8 | docker-compose.*.yml
9 | */bin
10 | */obj
11 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.34/02-new-indexes.sql:
--------------------------------------------------------------------------------
1 | CREATE INDEX IF NOT EXISTS idx_subscription_resourcefilter
2 | ON events.subscription ((resourcefilter));
3 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/jwtselfsignedcert.pfx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/altinn/altinn-events/main/test/Altinn.Platform.Events.Tests/jwtselfsignedcert.pfx
--------------------------------------------------------------------------------
/src/Events/Migration/v0.43/01-new-index.sql:
--------------------------------------------------------------------------------
1 | CREATE UNIQUE INDEX IF NOT EXISTS idx_events_id_source
2 | ON events.events ((cloudevent -> 'id'), (cloudevent -> 'source'));
--------------------------------------------------------------------------------
/test/k6/src/data/subscriptions/01-app-subscription.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "endPoint": "replaced with environment variable",
4 | "sourceFilter": "replaced during setup"
5 | }
6 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.36/01-new-indexes.sql:
--------------------------------------------------------------------------------
1 | CREATE INDEX IF NOT EXISTS idx_events_cloudevent_resource_sequenceno
2 | ON events.events ((cloudevent ->> 'resource'), sequenceno);
3 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.51/01-setup-grants.sql:
--------------------------------------------------------------------------------
1 | -- Grant permissions to platform_events role
2 | GRANT SELECT, INSERT, UPDATE, DELETE ON events.trace_log_y2026 TO platform_events;
3 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.49/01-new-index.sql:
--------------------------------------------------------------------------------
1 | CREATE INDEX IF NOT EXISTS idx_events_cloudevent_resource_time
2 | ON events.events ((cloudevent ->> 'resource'), (cloudevent ->> 'time'));
--------------------------------------------------------------------------------
/.github/labeler.yml:
--------------------------------------------------------------------------------
1 | # Labels for pull requests
2 |
3 | review/db-migration:
4 | - any:
5 | - changed-files:
6 | - any-glob-to-any-file:
7 | - 'src/Events/Migration/**'
--------------------------------------------------------------------------------
/src/Events/Migration/v0.39/01-new-indexes.sql:
--------------------------------------------------------------------------------
1 | CREATE INDEX IF NOT EXISTS idx_events_cloudevent_subject_time
2 | ON events.events ((cloudevent ->> 'subject'), (cloudevent ->> 'time'));
3 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.50/01-setup-new-partition.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS events.trace_log_y2026 PARTITION OF events.trace_log
2 | FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');
3 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.46/03-setup-table-partitions.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS events.trace_log_y2025 PARTITION OF events.trace_log
2 | FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
3 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.18/01-alter-table.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE events.events
2 | RENAME TO events_app;
3 |
4 |
5 | ALTER TABLE events.events_app
6 | RENAME CONSTRAINT events_pkey TO events_app_pkey;
7 |
8 |
--------------------------------------------------------------------------------
/src/Events/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "System": "Information",
6 | "Microsoft": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Events/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/Events/Migration/v0.27/01-drop-function.sql:
--------------------------------------------------------------------------------
1 | drop function if exists events.getevents(_subject character varying, _alternativesubject character varying, _after character varying, _type text[], _source text[], _size integer);
--------------------------------------------------------------------------------
/src/Events/Migration/v0.44/02-setup-grants.sql:
--------------------------------------------------------------------------------
1 | GRANT SELECT, INSERT, UPDATE, REFERENCES, DELETE, TRUNCATE, TRIGGER ON events.trace_log TO platform_events;
2 |
3 | GRANT ALL ON events.trace_log_sequenceno_seq TO platform_events;
4 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.46/02-setup-grants.sql:
--------------------------------------------------------------------------------
1 | GRANT SELECT, INSERT, UPDATE, REFERENCES, DELETE, TRUNCATE, TRIGGER ON events.trace_log TO platform_events;
2 |
3 | GRANT ALL ON events.trace_log_sequenceno_seq TO platform_events;
4 |
--------------------------------------------------------------------------------
/src/Events/appsettings.Production.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Warning",
5 | "System": "Warning",
6 | "Microsoft": "Warning"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/test/k6/src/data/subscriptions/02-generic-subscription.json:
--------------------------------------------------------------------------------
1 | {
2 | "endPoint": "webhookEndpoint",
3 | "consumer": "norsknettavisleser",
4 | "resourceFilter":"urn:altinn:resource:ttd-altinn-events-automated-tests"
5 | }
6 |
--------------------------------------------------------------------------------
/container-scan.md:
--------------------------------------------------------------------------------
1 | # Status for container scans
2 |
3 | [](https://github.com/Altinn/altinn-events/actions/workflows/events-scan.yml)
4 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.37/00-drop-function.sql:
--------------------------------------------------------------------------------
1 | drop function if exists events.getevents(_subject character varying, _alternativesubject character varying, _after character varying, _type text[], _source character varying, _size integer);
--------------------------------------------------------------------------------
/src/Events/Migration/v0.21/04-setup-grants.sql:
--------------------------------------------------------------------------------
1 | GRANT SELECT,INSERT,UPDATE,REFERENCES,DELETE,TRUNCATE,REFERENCES,TRIGGER ON ALL TABLES IN SCHEMA events TO platform_events;
2 | GRANT ALL ON ALL SEQUENCES IN SCHEMA events TO platform_events;
3 |
4 |
5 |
--------------------------------------------------------------------------------
/test/k6/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | networks:
4 | k6:
5 |
6 | services:
7 | k6:
8 | image: grafana/k6:1.4.2
9 | networks:
10 | - k6
11 | ports:
12 | - "6565:6565"
13 | volumes:
14 | - ./src:/src
15 |
--------------------------------------------------------------------------------
/src/Events.Functions/Properties/serviceDependencies.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "appInsights1": {
4 | "type": "appInsights"
5 | },
6 | "storage1": {
7 | "type": "storage",
8 | "connectionId": "AzureWebJobsStorage"
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/src/Events/Migration/v0.21/01-setup-table.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS events.events (
2 | sequenceno BIGSERIAL,
3 | cloudevent JSONB NOT NULL,
4 | registeredtime timestamptz default (now() at time zone 'utc'),
5 | CONSTRAINT events_pkey PRIMARY KEY (sequenceno)
6 | );
--------------------------------------------------------------------------------
/src/Events/Migration/v0.21/02-setup-index.sql:
--------------------------------------------------------------------------------
1 | CREATE INDEX IF NOT EXISTS idx_gin_events_computed_cloudevent ON events.events USING GIN (cloudevent jsonb_path_ops);
2 |
3 | CREATE INDEX IF NOT EXISTS idx_events_computed_time ON events.events USING btree ("registeredtime" ASC NULLS LAST);
4 |
--------------------------------------------------------------------------------
/src/Events.Functions/Properties/serviceDependencies.local.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "appInsights1": {
4 | "type": "appInsights.sdk"
5 | },
6 | "storage1": {
7 | "type": "storage.emulator",
8 | "connectionId": "AzureWebJobsStorage"
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/src/Events/Migration/FunctionsAndProcedures/deletesubscription.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE PROCEDURE events.deletesubscription(_id integer)
2 | LANGUAGE 'plpgsql'
3 |
4 | AS $BODY$
5 | BEGIN
6 | DELETE
7 | FROM events.subscription s
8 | where s.id = _id;
9 |
10 | END;
11 | $BODY$;
--------------------------------------------------------------------------------
/src/Events/Migration/v0.00/02-setup-grants.sql:
--------------------------------------------------------------------------------
1 | GRANT USAGE ON SCHEMA events TO platform_events;
2 | GRANT SELECT,INSERT,UPDATE,REFERENCES,DELETE,TRUNCATE,REFERENCES,TRIGGER ON ALL TABLES IN SCHEMA events TO platform_events;
3 | GRANT ALL ON ALL SEQUENCES IN SCHEMA events TO platform_events;
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.03/03-setup-grants.sql:
--------------------------------------------------------------------------------
1 | GRANT USAGE ON SCHEMA events TO platform_events;
2 | GRANT SELECT,INSERT,UPDATE,REFERENCES,DELETE,TRUNCATE,REFERENCES,TRIGGER ON ALL TABLES IN SCHEMA events TO platform_events;
3 | GRANT ALL ON ALL SEQUENCES IN SCHEMA events TO platform_events;
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.06/01-setup-functions.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE PROCEDURE events.setvalidsubscription(_id integer)
2 | LANGUAGE 'plpgsql'
3 |
4 | AS $BODY$
5 | BEGIN
6 | UPDATE
7 | events.subscription
8 | SET
9 | validated = true
10 | WHERE id = _id;
11 | END;
12 | $BODY$;
13 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.29/01-drop-resources.sql:
--------------------------------------------------------------------------------
1 | drop procedure if exists events.insertappevent(IN id character varying, IN source character varying, IN subject character varying, IN type character varying, IN "time" timestamp with time zone, IN cloudevent text);
2 |
3 | drop table if exists events.events_app;
--------------------------------------------------------------------------------
/src/DbTools/DbTools.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net10.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.19/01-setup-cron-job.sql:
--------------------------------------------------------------------------------
1 | DO
2 | $do$
3 | BEGIN
4 | IF EXISTS (SELECT setting FROM pg_settings WHERE name = 'azure.customer_resource_group') THEN
5 | SELECT cron.schedule('0 3 * * *', $$DELETE FROM events.events_app WHERE time < now() - interval '90 days'$$);
6 | END IF;
7 | END
8 | $do$
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Profile/User/1.json:
--------------------------------------------------------------------------------
1 | {
2 | "UserId": 1,
3 | "UserName": "OlaNordmann",
4 | "PhoneNumber": "12345678",
5 | "Email": "test@test.com",
6 | "PartyId": 1,
7 | "Party": {
8 |
9 | },
10 | "UserType": 0,
11 | "ProfileSettingPreference": {
12 |
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.21/03-setup-cron-job.sql:
--------------------------------------------------------------------------------
1 | DO
2 | $do$
3 | BEGIN
4 | IF EXISTS (SELECT setting FROM pg_settings WHERE name = 'azure.customer_resource_group') THEN
5 | SELECT cron.schedule('0 3 * * *', $$DELETE FROM events.events WHERE registeredtime < now() - interval '90 days'$$);
6 | END IF;
7 | END
8 | $do$
9 |
--------------------------------------------------------------------------------
/src/Events/Migration/FunctionsAndProcedures/setvalidsubscription.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE PROCEDURE events.setvalidsubscription(_id integer)
2 | LANGUAGE 'plpgsql'
3 |
4 | AS $BODY$
5 | BEGIN
6 | UPDATE
7 | events.subscription
8 | SET
9 | validated = true
10 | WHERE id = _id;
11 | END;
12 | $BODY$;
13 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Profile/User/12345.json:
--------------------------------------------------------------------------------
1 | {
2 | "UserId": 12345,
3 | "UserName": "OlaNordmann",
4 | "PhoneNumber": "12345678",
5 | "Email": "test@test.com",
6 | "PartyId": 12345,
7 | "Party": {
8 |
9 | },
10 | "UserType": 0,
11 | "ProfileSettingPreference": {
12 |
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.04/01-setup-index.sql:
--------------------------------------------------------------------------------
1 | CREATE EXTENSION IF NOT EXISTS pg_trgm;
2 | CREATE INDEX IF NOT EXISTS idx_gin_subscription_consumer ON events.subscription USING gin (consumer gin_trgm_ops);
3 |
4 | CREATE INDEX IF NOT EXISTS idx_subscription_subject_source_type ON events.subscription(subjectfilter, sourcefilter, typefilter)
--------------------------------------------------------------------------------
/src/Events.Common/Altinn.Platform.Events.Common.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net10.0
5 | enable
6 | enable
7 | Library
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Profile/User/1337.json:
--------------------------------------------------------------------------------
1 | {
2 | "UserId": 1337,
3 | "UserName": "SophieDDG",
4 | "PhoneNumber": "90001337",
5 | "Email": "1337@altinnstudiotestusers.com",
6 | "PartyId": 1337,
7 | "Party": {
8 |
9 | },
10 | "UserType": 1,
11 | "ProfileSettingPreference": {
12 |
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.23/02-alter-table.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE events.subscription
2 | ADD sourcefilterhash character varying(32);
3 |
4 | CREATE INDEX IF NOT EXISTS idx_btree_subscription_sourcefilterhash ON events.subscription USING btree (sourcefilterhash);
5 |
6 | UPDATE events.subscription
7 | SET sourcefilterhash=Upper(MD5(sourcefilter));
8 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Profile/User/21023.json:
--------------------------------------------------------------------------------
1 | {
2 | "UserId": 21023,
3 | "UserName": "SophieDDG",
4 | "PhoneNumber": "90001337",
5 | "Email": "1337@altinnstudiotestusers.com",
6 | "PartyId": 1337,
7 | "Party": {
8 |
9 | },
10 | "UserType": 1,
11 | "ProfileSettingPreference": {
12 |
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/test/k6/src/data/events/01-event.json:
--------------------------------------------------------------------------------
1 | {
2 | "source":"https://github.com/Altinn/altinn-events/tree/main/test/k6",
3 | "specversion":"1.0",
4 | "type":"automatedtest.triggered",
5 | "subject":"/autotest/k6",
6 | "resource":"urn:altinn:resource:ttd-altinn-events-automated-tests",
7 | "time": "2022-05-12T00:02:07.541482Z"
8 | }
--------------------------------------------------------------------------------
/test/k6/src/data/app-events/01-app-event.json:
--------------------------------------------------------------------------------
1 | {
2 | "source": "https://ttd.apps.{0}/ttd/apps-test/instances/{1}/573993d4-af79-4c5e-b852-85cd691ee7fd",
3 | "specversion": "1.0",
4 | "type": "app.instance.created",
5 | "subject": "/party/{0}",
6 | "time": "2022-11-18T09:20:02.931677Z",
7 | "alternativesubject": "/person/{0}"
8 | }
--------------------------------------------------------------------------------
/.github/workflows/regression-test-AT22.yaml:
--------------------------------------------------------------------------------
1 | name: Regression Test - AT22
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: '0 12 * * 1-5'
7 |
8 | jobs:
9 | at22:
10 | permissions:
11 | contents: read
12 | uses: ./.github/workflows/regression-test-ATX.yml
13 | with:
14 | environment: AT22
15 | secrets: inherit
16 |
--------------------------------------------------------------------------------
/.github/workflows/regression-test-AT23.yaml:
--------------------------------------------------------------------------------
1 | name: Regression Test - AT23
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: '0 12 * * 1-5'
7 |
8 | jobs:
9 | at23:
10 | permissions:
11 | contents: read
12 | uses: ./.github/workflows/regression-test-ATX.yml
13 | with:
14 | environment: AT23
15 | secrets: inherit
16 |
--------------------------------------------------------------------------------
/.github/workflows/regression-test-AT24.yaml:
--------------------------------------------------------------------------------
1 | name: Regression Test - AT24
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: '0 12 * * 1-5'
7 |
8 | jobs:
9 | at24:
10 | permissions:
11 | contents: read
12 | uses: ./.github/workflows/regression-test-ATX.yml
13 | with:
14 | environment: AT24
15 | secrets: inherit
16 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Roles/User_1337/party_500700/roles.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "Type": "altinn",
4 | "value": "MEDL"
5 | },
6 | {
7 | "Type": "altinn",
8 | "value": "REGNA"
9 | },
10 | {
11 | "Type": "altinn",
12 | "value": "UTINN"
13 | },
14 | {
15 | "Type": "altinn",
16 | "value": "UTOMR"
17 | }
18 | ]
19 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Register/Party/1337.json:
--------------------------------------------------------------------------------
1 | {
2 | "PartyId": "1337",
3 | "PartyTypeName": 1,
4 | "OrgNumber": null,
5 | "SSN": "01039012345",
6 | "UnitType": null,
7 | "Name": "Sophie Salt",
8 | "IsDeleted": false,
9 | "OnlyHierarchyElementWithNoAccess": false,
10 | "Person": null,
11 | "Organization": null,
12 | "ChildParties": null
13 | }
14 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Register/Party/1000.json:
--------------------------------------------------------------------------------
1 | {
2 | "PartyId": "1000",
3 | "PartyTypeName": 1,
4 | "OrgNumber": null,
5 | "SSN": "12345678901",
6 | "UnitType": null,
7 | "Name": "Ola Nordmann",
8 | "IsDeleted": false,
9 | "OnlyHierarchyElementWithNoAccess": false,
10 | "Person": null,
11 | "Organization": null,
12 | "ChildParties": null
13 | }
14 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Register/Party/1001.json:
--------------------------------------------------------------------------------
1 | {
2 | "PartyId": "1001",
3 | "PartyTypeName": 1,
4 | "OrgNumber": null,
5 | "SSN": "12345678901",
6 | "UnitType": null,
7 | "Name": "Tore Nordmann",
8 | "IsDeleted": false,
9 | "OnlyHierarchyElementWithNoAccess": false,
10 | "Person": null,
11 | "Organization": null,
12 | "ChildParties": null
13 | }
14 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Register/Party/500000.json:
--------------------------------------------------------------------------------
1 | {
2 | "partyId": "500000",
3 | "partyTypeName": 2,
4 | "orgNumber": "897069650",
5 | "ssn": null,
6 | "unitType": "AS",
7 | "name": "DDG Fitness",
8 | "isDeleted": false,
9 | "onlyHierarchyElementWithNoAccess": false,
10 | "person": null,
11 | "organisation": null,
12 | "childParties": null
13 | }
14 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/PartiesRegisterQueryResponse/oneperson.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": [
3 | {
4 | "partyType": "person",
5 | "partyUuid": "4a80af94-14be-4af5-9f95-a6a0824c5b55",
6 | "partyId": 50067466,
7 | "personIdentifier": "18874198354",
8 | "organizationIdentifier": null
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Register/Party/1002.json:
--------------------------------------------------------------------------------
1 | {
2 | "PartyId": "1002",
3 | "PartyTypeName": 2,
4 | "OrgNumber": "897069631",
5 | "SSN": null,
6 | "UnitType": "AS",
7 | "Name": "EAS Health Consulting",
8 | "IsDeleted": false,
9 | "OnlyHierarchyElementWithNoAccess": false,
10 | "Person": null,
11 | "Organization": null,
12 | "ChildParties": null
13 | }
14 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Register/Party/500001.json:
--------------------------------------------------------------------------------
1 | {
2 | "partyId": "500001",
3 | "partyTypeName": 2,
4 | "orgNumber": "897069651",
5 | "ssn": null,
6 | "unitType": "BEDR",
7 | "name": "DDG Fitness Oslo",
8 | "isDeleted": false,
9 | "onlyHierarchyElementWithNoAccess": false,
10 | "person": null,
11 | "organisation": null,
12 | "childParties": null
13 | }
14 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.08/01-setup-index.sql:
--------------------------------------------------------------------------------
1 | CREATE EXTENSION IF NOT EXISTS pg_trgm;
2 |
3 | CREATE INDEX IF NOT EXISTS idx_gin_events_source ON events.events USING gin (source gin_trgm_ops);
4 |
5 | CREATE INDEX IF NOT EXISTS idx_events_time ON events.events USING btree ("time" ASC NULLS LAST);
6 |
7 | CREATE INDEX IF NOT EXISTS idx_events_subject ON events.events USING btree (subject ASC NULLS LAST);
8 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Register/Party/500002.json:
--------------------------------------------------------------------------------
1 | {
2 | "partyId": "500002",
3 | "partyTypeName": 2,
4 | "orgNumber": "897069652",
5 | "ssn": null,
6 | "unitType": "BEDR",
7 | "name": "DDG Fitness Bergen",
8 | "isDeleted": false,
9 | "onlyHierarchyElementWithNoAccess": false,
10 | "person": null,
11 | "organisation": null,
12 | "childParties": null
13 | }
14 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Register/Party/500003.json:
--------------------------------------------------------------------------------
1 | {
2 | "partyId": "500003",
3 | "partyTypeName": 2,
4 | "orgNumber": "897069653",
5 | "ssn": null,
6 | "unitType": "BEDR",
7 | "name": "DDG Fitness Trondheim",
8 | "isDeleted": false,
9 | "onlyHierarchyElementWithNoAccess": false,
10 | "person": null,
11 | "organisation": null,
12 | "childParties": null
13 | }
14 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Register/Party/500600.json:
--------------------------------------------------------------------------------
1 | {
2 | "partyId": "500600",
3 | "partyTypeName": 2,
4 | "orgNumber": "897069631",
5 | "ssn": null,
6 | "unitType": "AS",
7 | "name": "EAS Health Consulting",
8 | "isDeleted": false,
9 | "onlyHierarchyElementWithNoAccess": false,
10 | "person": null,
11 | "organisation": null,
12 | "childParties": null
13 | }
14 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Register/Party/500700.json:
--------------------------------------------------------------------------------
1 | {
2 | "partyId": "500700",
3 | "partyTypeName": 2,
4 | "orgNumber": "950474084",
5 | "ssn": null,
6 | "unitType": "BRL",
7 | "name": "Oslos Vakreste Borettslag",
8 | "isDeleted": false,
9 | "onlyHierarchyElementWithNoAccess": false,
10 | "person": null,
11 | "organisation": null,
12 | "childParties": null
13 | }
14 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.24/01-cleanup-obsolete.sql:
--------------------------------------------------------------------------------
1 | drop function if exists events.getsubscriptions(source character varying, subject character varying, type character varying);
2 |
3 | drop function if exists events.insert_subscription(sourcefilter character varying, subjectfilter character varying, typefilter character varying, consumer character varying, endpointurl character varying, createdby character varying, validated boolean);
4 |
--------------------------------------------------------------------------------
/src/Events.Functions/Constants/EventConstants.cs:
--------------------------------------------------------------------------------
1 | namespace Altinn.Platform.Events.Functions.Constants;
2 |
3 | ///
4 | /// Shared constants for Events.Functions project
5 | ///
6 | public static class EventConstants
7 | {
8 | ///
9 | /// The cloud event type for subscription validation
10 | ///
11 | public const string ValidationType = "platform.events.validatesubscription";
12 | }
13 |
--------------------------------------------------------------------------------
/src/Events/Services/PartiesRegisterQueryRequest.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | namespace Altinn.Platform.Events.Services;
4 |
5 | ///
6 | /// Data type used for querying parties in register based on URN strings.
7 | ///
8 | public record PartiesRegisterQueryRequest
9 | {
10 | ///
11 | /// List of valid URN strings with party identifying values.
12 | ///
13 | public required string[] Data { get; set; }
14 | }
15 |
--------------------------------------------------------------------------------
/.github/workflows/assign-issues-to-projects.yml:
--------------------------------------------------------------------------------
1 | name: Auto Assign to Project
2 |
3 | on:
4 | issues:
5 | types:
6 | - opened
7 |
8 | jobs:
9 | add-to-project:
10 | name: Add issue to Team Platform project
11 | runs-on: ubuntu-latest
12 | permissions: {}
13 | steps:
14 | - uses: actions/add-to-project@main
15 | with:
16 | project-url: https://github.com/orgs/Altinn/projects/20
17 | github-token: ${{ secrets.ASSIGN_PROJECT_TOKEN }}
18 |
--------------------------------------------------------------------------------
/stylecop.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
3 | "settings": {
4 | "documentationRules": {
5 | "companyName": "PlaceholderCompany"
6 | },
7 | "orderingRules": {
8 | "usingDirectivesPlacement": "outsideNamespace",
9 | "systemUsingDirectivesFirst": true,
10 | "blankLinesBetweenUsingGroups": "allow"
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Events.Functions/Configuration/PlatformSettings.cs:
--------------------------------------------------------------------------------
1 | namespace Altinn.Platform.Events.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 Events API endpoint.
10 | ///
11 | public string ApiEventsEndpoint { get; set; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Events.Functions/Configuration/CertificateResolverSettings.cs:
--------------------------------------------------------------------------------
1 | namespace Altinn.Platform.Events.Functions.Configuration
2 | {
3 | ///
4 | /// Configuration object used to hold settings for the CertificateResolver.
5 | ///
6 | public class CertificateResolverSettings
7 | {
8 | ///
9 | /// Certificatee cache life time
10 | ///
11 | public int CacheCertLifetimeInSeconds { get; set; } = 3600;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.4'
2 |
3 | networks:
4 | altinnplatform_network:
5 | external: false
6 |
7 | services:
8 | altinn_platform_events:
9 | container_name: altinn-events
10 | image: altinn-events:latest
11 | restart: always
12 | networks:
13 | - altinnplatform_network
14 | environment:
15 | - ASPNETCORE_ENVIRONMENT=Development
16 | - ASPNETCORE_URLS=http://+:5080
17 | ports:
18 | - "5080:5080"
19 | build:
20 | context: .
21 | dockerfile: Dockerfile
22 |
--------------------------------------------------------------------------------
/src/Events/Repository/ITraceLogRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | using Altinn.Platform.Events.Models;
4 |
5 | namespace Altinn.Platform.Events.Repository
6 | {
7 | ///
8 | /// Interface for the trace log repository
9 | ///
10 | public interface ITraceLogRepository
11 | {
12 | ///
13 | /// Creates a trace log entry
14 | ///
15 | ///
16 | Task CreateTraceLogEntry(TraceLog traceLog);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Events.Functions/Configuration/EventsOutboundSettings.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | namespace Altinn.Platform.Events.Functions.Configuration;
4 |
5 | ///
6 | /// Configuration object used to hold settings for the EventsOutbound function and related services.
7 | ///
8 | public class EventsOutboundSettings
9 | {
10 | ///
11 | /// The number of seconds the event push http client will wait for a response before timing out.
12 | ///
13 | public int RequestTimeout { get; set; } = 300;
14 | }
15 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/GlobalSuppressions.cs:
--------------------------------------------------------------------------------
1 | // This file is used by Code Analysis to maintain SuppressMessage
2 | // attributes that are applied to this project.
3 | // Project-level suppressions either have no target or are given
4 | // a specific target and scoped to a namespace, type, member, etc.
5 |
6 | using System.Diagnostics.CodeAnalysis;
7 |
8 | [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Test description should be in the name of the test.", Scope = "module")]
9 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Functions.Tests/GlobalSuppressions.cs:
--------------------------------------------------------------------------------
1 | // This file is used by Code Analysis to maintain SuppressMessage
2 | // attributes that are applied to this project.
3 | // Project-level suppressions either have no target or are given
4 | // a specific target and scoped to a namespace, type, member, etc.
5 |
6 | using System.Diagnostics.CodeAnalysis;
7 |
8 | [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Test description should be in the name of the test.", Scope = "module")]
9 |
--------------------------------------------------------------------------------
/src/Events.Functions/Services/Interfaces/IWebhookService.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | using Altinn.Platform.Events.Functions.Models;
4 |
5 | namespace Altinn.Platform.Events.Functions.Services.Interfaces;
6 |
7 | ///
8 | /// Interface to send content to webhooks
9 | ///
10 | public interface IWebhookService
11 | {
12 | ///
13 | /// Send cloudevent to webhook
14 | ///
15 | /// CloudEventEnvelope, includes content and uri
16 | Task Send(CloudEventEnvelope envelope);
17 | }
18 |
--------------------------------------------------------------------------------
/src/Events/Mappers/SubscriptionMapper.cs:
--------------------------------------------------------------------------------
1 | using Altinn.Platform.Events.Models;
2 |
3 | namespace Altinn.Platform.Events.Mappers
4 | {
5 | ///
6 | /// A class that holds the subscription mapper configurations
7 | ///
8 | public class SubscriptionMapper : AutoMapper.Profile
9 | {
10 | ///
11 | /// The subscription mapper configuration
12 | ///
13 | public SubscriptionMapper()
14 | {
15 | CreateMap();
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Events/Configuration/PostgreSQLSettings.cs:
--------------------------------------------------------------------------------
1 | namespace Altinn.Platform.Events.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 EventsDbPwd { get; set; }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.github/workflows/pr-labeler.yml:
--------------------------------------------------------------------------------
1 | name: Label critical PR
2 | on:
3 | pull_request_target:
4 | branches:
5 | - main
6 | types:
7 | - opened
8 | paths:
9 | - '**/Migration/**'
10 |
11 | jobs:
12 | add_labels:
13 |
14 | runs-on: ubuntu-latest
15 | permissions:
16 | contents: read
17 | pull-requests: write
18 | steps:
19 | - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
20 | - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
21 | with:
22 | repo-token: "${{ secrets.GITHUB_TOKEN }}"
23 |
--------------------------------------------------------------------------------
/src/Events/Models/QueuePostReceipt.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Altinn.Platform.Events.Models
4 | {
5 | ///
6 | /// Object to hold the receipt for a push queue action.
7 | ///
8 | public class QueuePostReceipt
9 | {
10 | ///
11 | /// Boolean to indicate if the push was successful.
12 | ///
13 | public bool Success { get; set; }
14 |
15 | ///
16 | /// Exception. Only populated if the push failed.
17 | ///
18 | public Exception Exception { get; set; }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.17/01-setup-functions.sql:
--------------------------------------------------------------------------------
1 | DROP FUNCTION IF EXISTS events.insertevent(character varying, character varying, character varying, character varying, timestamptz, text);
2 |
3 | CREATE OR REPLACE PROCEDURE events.insertevent(
4 | id character varying,
5 | source character varying,
6 | subject character varying,
7 | type character varying,
8 | "time" timestamptz,
9 | cloudevent text)
10 | LANGUAGE 'plpgsql'
11 |
12 | AS $BODY$
13 |
14 | BEGIN
15 | INSERT INTO events.events(id, source, subject, type, "time", cloudevent)
16 | VALUES ($1, $2, $3, $4, $5, $6);
17 | END;
18 |
19 | $BODY$;
20 |
--------------------------------------------------------------------------------
/src/Events/Models/SubscriptionList.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Altinn.Platform.Events.Models
4 | {
5 | ///
6 | /// An object containing a list of subscriptions and metadata
7 | ///
8 | public class SubscriptionList
9 | {
10 | ///
11 | /// The nuber of subscriptions in the list
12 | ///
13 | public int Count { get; set; }
14 |
15 | ///
16 | /// The list of subscriptions
17 | ///
18 | public List Subscriptions { get; set; }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Events.Functions/Configuration/KeyVaultSettings.cs:
--------------------------------------------------------------------------------
1 | namespace Altinn.Platform.Events.Functions.Configuration
2 | {
3 | ///
4 | /// Configuration object used to hold settings for the KeyVault.
5 | ///
6 | public class KeyVaultSettings
7 | {
8 | ///
9 | /// Uri to keyvault
10 | ///
11 | public string KeyVaultURI { get; set; }
12 |
13 | ///
14 | /// Name of the certificate secret
15 | ///
16 | public string PlatformCertSecretId { get; set; } = "platform-access-token-private-cert";
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/appsettings.unittest.json:
--------------------------------------------------------------------------------
1 | {
2 | "PostgreSQLSettings": {
3 | "EnableDBConnection": "false"
4 | },
5 | "GeneralSettings": {
6 | "BaseUri": "http://localhost:5080",
7 | "OpenIdWellKnownEndpoint": "http://localhost:5101/authentication/api/v1/openid/",
8 | "JwtCookieName": "AltinnStudioRuntime"
9 | },
10 | "PlatformSettings": {
11 | "RegisterApiBaseAddress": "http://localhost:5101/register/api/",
12 | "ApiAuthorizationEndpoint": "http://localhost:5050/authorization/api/v1/",
13 | "AppsDomain": "apps.altinn.no",
14 | "SubscriptionCachingLifetimeInSeconds": 3600
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/PartiesRegisterQueryResponse/twopersons.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": [
3 | {
4 | "partyType": "person",
5 | "partyUuid": "8be970b6-b361-42c6-9b53-5cd3ab5efbe9",
6 | "partyId": 50002207,
7 | "personIdentifier": "02056241046",
8 | "organizationIdentifier": null
9 | },
10 | {
11 | "partyType": "person",
12 | "partyUuid": "930423fe-8bbd-43f0-bf4c-3ddf5a5eb4e7",
13 | "partyId": 50023810,
14 | "personIdentifier": "31073102351",
15 | "organizationIdentifier": null
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/src/Events.Functions/host.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0",
3 | "logging": {
4 | "applicationInsights": {
5 | "samplingSettings": {
6 | "isEnabled": true,
7 | "excludedTypes": "Request"
8 | }
9 | },
10 | "logLevel": {
11 | "default": "Information",
12 | "Function": "Debug",
13 | "Altinn.Platform.Events.Functions.Services.PushEventsService": "Information"
14 | }
15 | },
16 | "extensions": {
17 | "queues": {
18 | "messageEncoding": "base64",
19 | "maxPollingInterval": "00:00:02",
20 | "visibilityTimeout": "00:00:30",
21 | "maxDequeueCount": 5
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Events.Functions/local.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "IsEncrypted": false,
3 | "Values": {
4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true",
5 | "QueueStorage": "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1",
6 | "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
7 | "ExponentialRetryBackoff:QueueName": "events-outbound",
8 | "Platform:ApiEventsEndpoint": "http://localhost:5080/events/api/v1/",
9 | "KeyVault:KeyVaultURI": "https://altinn-at22-kv.vault.azure.net/"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Events.Functions/Services/Interfaces/ICertificateResolverService.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Cryptography.X509Certificates;
2 | using System.Threading.Tasks;
3 |
4 | namespace Altinn.Platform.Events.Functions.Services.Interfaces
5 | {
6 | ///
7 | /// Interface to retrive certificate for access token
8 | ///
9 | public interface ICertificateResolverService
10 | {
11 | ///
12 | /// Returns certificate to be used for signing an access token
13 | ///
14 | /// The signing credentials
15 | public Task GetCertificateAsync();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/events/events.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "4665ff57-93d5-4bb4-83a2-8355a1cb686a",
4 | "source": "urn:altinn:systemx",
5 | "specversion": "1.0",
6 | "type": "instance.deleted",
7 | "subject": "/party/1337",
8 | "alternativesubject": "/person/01038712345",
9 | "time": "2020-10-13T11:50:29Z"
10 | },
11 | {
12 | "id": "5fac27e8-6ca8-4ab0-bea5-938ba6c1016f",
13 | "source": "urn:altinn:systemx",
14 | "specversion": "1.0",
15 | "type": "instance.deleted",
16 | "subject": "/party/1337",
17 | "alternativesubject": "/person/01038712345",
18 | "time": "2020-10-13T12:50:29Z"
19 | }
20 | ]
21 |
--------------------------------------------------------------------------------
/src/Events/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:53961/",
8 | "sslPort": 44359
9 | }
10 | },
11 | "profiles": {
12 | "Altinn.Platform.Events": {
13 | "commandName": "Project",
14 | "launchBrowser": true,
15 | "launchUrl": "swagger",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | },
19 | "applicationUrl": "http://localhost:5080"
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.22/01-cleanup-obsolete.sql:
--------------------------------------------------------------------------------
1 | DROP FUNCTION IF EXISTS events.get(character varying, character varying, timestamp with time zone, timestamp with time zone, text[], text[]);
2 |
3 | DROP FUNCTION IF EXISTS events.get(character varying, character varying, timestamp with time zone, timestamp with time zone, text[], text[], integer);
4 |
5 | DROP FUNCTION IF EXISTS events.getsubscriptionsbyconsumer(character varying);
6 |
7 | DROP PROCEDURE IF EXISTS events.insert_event(character varying, character varying, character varying, character varying, text);
8 |
9 | DROP FUNCTION IF EXISTS events.insertevent(character varying, character varying, character varying, character varying, text);
10 |
--------------------------------------------------------------------------------
/test/Postman/Platform Events - localhost.postman_environment.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "9eeab3e9-1a3b-46cc-a232-cc16ace1b339",
3 | "name": "Platform Events - localhost",
4 | "values": [
5 | {
6 | "key": "PlatformHostUrl",
7 | "value": "http://localhost:5080",
8 | "type": "default",
9 | "enabled": true
10 | },
11 | {
12 | "key": "AltinnSlackWebHook",
13 | "value": "",
14 | "type": "secret",
15 | "enabled": true
16 | },
17 | {
18 | "key": "PlatformToken",
19 | "value": "",
20 | "type": "secret",
21 | "enabled": true
22 | }
23 | ],
24 | "_postman_variable_scope": "environment",
25 | "_postman_exported_at": "2022-11-18T13:29:10.252Z",
26 | "_postman_exported_using": "Postman/10.2.2"
27 | }
--------------------------------------------------------------------------------
/src/Events/Migration/FunctionsAndProcedures/getsubscription.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.getsubscription_v2(
2 | _id integer)
3 | RETURNS TABLE(id bigint, resourcefilter character varying, sourcefilter character varying, subjectfilter character varying, typefilter character varying, consumer character varying, endpointurl character varying, createdby character varying, validated boolean, "time" timestamp with time zone)
4 | LANGUAGE 'plpgsql'
5 | AS $BODY$
6 |
7 | BEGIN
8 | return query
9 | SELECT s.id, s.resourcefilter, s.sourcefilter, s.subjectfilter, s.typefilter, s.consumer, s.endpointurl, s.createdby, s.validated, s."time"
10 | FROM events.subscription s
11 | where s.id = _id;
12 |
13 | END;
14 | $BODY$;
--------------------------------------------------------------------------------
/src/Events.Functions/TelemetryInitializer.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.ApplicationInsights.Channel;
2 | using Microsoft.ApplicationInsights.Extensibility;
3 |
4 | namespace Altinn.Platform.Events.Functions
5 | {
6 | ///
7 | /// Class that handles initialization of App Insights telemetry.
8 | ///
9 | public class TelemetryInitializer : ITelemetryInitializer
10 | {
11 | ///
12 | /// Initializer.
13 | ///
14 | /// The telemetry.
15 | public void Initialize(ITelemetry telemetry)
16 | {
17 | // set custom role name here
18 | telemetry.Context.Cloud.RoleName = "events-function";
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Events/Configuration/GeneralSettings.cs:
--------------------------------------------------------------------------------
1 | namespace Altinn.Platform.Events.Configuration
2 | {
3 | ///
4 | /// Configuration object used to hold general settings for the events application.
5 | ///
6 | public class GeneralSettings
7 | {
8 | ///
9 | /// Base Uri
10 | ///
11 | public string BaseUri { get; set; }
12 |
13 | ///
14 | /// Open Id Connect Well known endpoint
15 | ///
16 | public string OpenIdWellKnownEndpoint { get; set; }
17 |
18 | ///
19 | /// Name of the cookie for where JWT is stored
20 | ///
21 | public string JwtCookieName { get; set; }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.18/02-alter-procedure.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE PROCEDURE events.insertevent(
2 | id character varying,
3 | source character varying,
4 | subject character varying,
5 | type character varying,
6 | "time" timestamptz,
7 | cloudevent text)
8 | LANGUAGE 'plpgsql'
9 |
10 | AS $BODY$
11 |
12 | BEGIN
13 | INSERT INTO events.events_app(id, source, subject, type, "time", cloudevent)
14 | VALUES ($1, $2, $3, $4, $5, $6);
15 | END;
16 |
17 | $BODY$;
18 |
19 |
20 | ALTER PROCEDURE events.insertevent(
21 | IN id character varying,
22 | IN source character varying,
23 | IN subject character varying,
24 | IN type character varying,
25 | IN "time" timestamp with time zone,
26 | IN cloudevent text)
27 | RENAME TO insertappevent;
28 |
29 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.44/01-setup-tables.sql:
--------------------------------------------------------------------------------
1 | -- Table: events.trace_log
2 |
3 | CREATE TABLE IF NOT EXISTS events.trace_log
4 | (
5 | cloudeventid uuid NOT NULL,
6 | resource character varying COLLATE pg_catalog."default",
7 | eventtype character varying COLLATE pg_catalog."default",
8 | consumer character varying COLLATE pg_catalog."default",
9 | "time" timestamp with time zone DEFAULT (now() AT TIME ZONE 'utc'::text),
10 | subscriptionid bigint,
11 | responsecode integer,
12 | subscriberendpoint character varying COLLATE pg_catalog."default",
13 | activity character varying COLLATE pg_catalog."default",
14 | sequenceno BIGSERIAL,
15 | CONSTRAINT trace_log_pkey PRIMARY KEY (sequenceno)
16 | )
17 |
18 | TABLESPACE pg_default;
19 |
--------------------------------------------------------------------------------
/src/Events/Services/Interfaces/IClaimsPrincipalProvider.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Claims;
2 |
3 | namespace Altinn.Platform.Events.Services.Interfaces
4 | {
5 | ///
6 | /// Defines the methods required for an implementation of a user JSON Web Token provider.
7 | /// The provider is used by client implementations that needs the user token in requests
8 | /// against other systems.
9 | ///
10 | public interface IClaimsPrincipalProvider
11 | {
12 | ///
13 | /// Defines a method that can return a claims principal for the current user.
14 | ///
15 | /// The Json Web Token for the current user.
16 | public ClaimsPrincipal GetUser();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Events.Functions/Services/Interfaces/IRetryBackoffService.cs:
--------------------------------------------------------------------------------
1 | using Altinn.Platform.Events.Common.Models;
2 |
3 | namespace Altinn.Platform.Events.Functions.Services.Interfaces;
4 |
5 | ///
6 | /// Service for managing retry backoff strategies for failed message processing.
7 | ///
8 | public interface IRetryBackoffService
9 | {
10 | ///
11 | /// Requeues a failed message with appropriate backoff delay.
12 | ///
13 | /// The message to requeue.
14 | /// The exception that caused processing failure.
15 | /// Task representing the requeue operation.
16 | Task RequeueWithBackoff(RetryableEventWrapper message, Exception exception);
17 | }
18 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.46/01-setup-tables.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS events.trace_log;
2 |
3 | CREATE TABLE IF NOT EXISTS events.trace_log
4 | (
5 | cloudeventid uuid NOT NULL,
6 | resource text COLLATE pg_catalog."default",
7 | eventtype text COLLATE pg_catalog."default",
8 | consumer text COLLATE pg_catalog."default",
9 | "time" timestamp with time zone NOT NULL DEFAULT (now() AT TIME ZONE 'utc'::text),
10 | subscriptionid bigint,
11 | responsecode integer,
12 | subscriberendpoint text COLLATE pg_catalog."default",
13 | activity text COLLATE pg_catalog."default",
14 | sequenceno BIGSERIAL NOT NULL,
15 | CONSTRAINT trace_log_pkey PRIMARY KEY (sequenceno, "time")
16 | ) PARTITION BY RANGE ("time");
17 |
18 | CREATE INDEX ON events.trace_log (time);
19 |
--------------------------------------------------------------------------------
/test/k6/src/errorhandler.js:
--------------------------------------------------------------------------------
1 | import { Counter } from "k6/metrics";
2 | import { fail } from "k6";
3 |
4 | let ErrorCount = new Counter("errors");
5 |
6 | //Adds a count to the error counter when value of success is false
7 | export function addErrorCount(success) {
8 | if (!success) {
9 | ErrorCount.add(1);
10 | }
11 | }
12 |
13 | /**
14 | * Stops k6 iteration when success is false and prints test name with response code
15 | * @param {String} testName
16 | * @param {boolean} success
17 | * @param {JSON} res
18 | */
19 | export function stopIterationOnFail(testName, success, res) {
20 | if (!success && res != null) {
21 | fail(testName + ": Response code: " + res.status + ". Response message: " + res.body);
22 | } else if (!success) {
23 | fail(testName);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Utils/TestDataLoader.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Text.Json;
3 | using System.Threading.Tasks;
4 |
5 | namespace Altinn.Platform.Events.Tests.Utils;
6 |
7 | public static class TestDataLoader
8 | {
9 | private static readonly JsonSerializerOptions _options = new()
10 | {
11 | PropertyNameCaseInsensitive = true
12 | };
13 |
14 | public static async Task Load(string id)
15 | {
16 | string path = $"../../../Data/{typeof(T).Name}/{id}.json";
17 |
18 | if (!File.Exists(path))
19 | {
20 | return default;
21 | }
22 |
23 | string fileContent = await File.ReadAllTextAsync(path);
24 |
25 | T data = JsonSerializer.Deserialize(fileContent, _options);
26 | return data;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Events.Functions/Services/Interfaces/IKeyVaultService.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Cryptography.X509Certificates;
2 | using System.Threading.Tasks;
3 |
4 | namespace Altinn.Platform.Events.Functions.Services.Interfaces
5 | {
6 | ///
7 | /// Interface for interacting with key vault
8 | ///
9 | public interface IKeyVaultService
10 | {
11 | ///
12 | /// Gets the certificate from the given key vault.
13 | ///
14 | /// The URI of the key vault to ask for secret.
15 | /// The id of the secret.
16 | /// The certificate as an X509Certificate2
17 | Task GetCertificateAsync(string vaultUri, string secretId);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Events/Controllers/WebhookReceiverController.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 |
3 | using Microsoft.AspNetCore.Mvc;
4 |
5 | using Swashbuckle.AspNetCore.Annotations;
6 |
7 | namespace Altinn.Platform.Events.Controllers
8 | {
9 | ///
10 | /// Controller for supporting automated tests.
11 | ///
12 | [ExcludeFromCodeCoverage]
13 | [Route("events/api/v1/tests/webhookreceiver")]
14 | [Consumes("application/json")]
15 | [SwaggerTag("Private API")]
16 | public class WebhookReceiverController : ControllerBase
17 | {
18 | ///
19 | /// Accepts an http post request and responds OK.
20 | ///
21 | [HttpPost]
22 | public ActionResult Post()
23 | {
24 | return Ok();
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.31/01-drop-function.sql:
--------------------------------------------------------------------------------
1 | drop function events.getsubscription(_id integer);
2 |
3 |
4 | drop function events.getsubscriptions(sourcehashset character varying[], subject character varying, type character varying);
5 |
6 |
7 | drop function events.getsubscriptionsbyconsumer(_consumer character varying, _includeinvalid boolean);
8 |
9 | drop function events.find_subscription(_sourcefilter character varying, _subjectfilter character varying, _typefilter character varying, _consumer character varying, _endpointurl character varying);
10 |
11 | drop function events.insert_subscription(sourcefilter character varying, subjectfilter character varying, typefilter character varying, consumer character varying, endpointurl character varying, createdby character varying, validated boolean, sourcefilterhash character varying);
--------------------------------------------------------------------------------
/src/Events/Migration/v0.05/01-setup-functions.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.insert_subscription(
2 | sourcefilter character varying,
3 | subjectfilter character varying,
4 | typefilter character varying,
5 | consumer character varying,
6 | endpointurl character varying,
7 | createdby character varying,
8 | validated boolean)
9 | RETURNS SETOF events.subscription AS
10 | $BODY$
11 | DECLARE currentTime timestamptz;
12 | BEGIN
13 | SET TIME ZONE UTC;
14 | currentTime := NOW();
15 |
16 | RETURN QUERY
17 | INSERT INTO events.subscription(sourcefilter, subjectfilter, typefilter, consumer, endpointurl, createdby, "time", validated)
18 | VALUES ($1, $2, $3, $4, $5, $6, currentTime, $7) RETURNING *;
19 |
20 | END
21 | $BODY$ LANGUAGE 'plpgsql';
22 |
23 | DROP PROCEDURE IF EXISTS events.insert_subcsription;
24 |
--------------------------------------------------------------------------------
/src/Events/Migration/FunctionsAndProcedures/insertsubscription.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.insert_subscription(
2 | resourcefilter character varying,
3 | sourcefilter character varying,
4 | subjectfilter character varying,
5 | typefilter character varying,
6 | consumer character varying,
7 | endpointurl character varying,
8 | createdby character varying,
9 | validated boolean)
10 | RETURNS SETOF events.subscription
11 | LANGUAGE 'plpgsql'
12 |
13 | AS $BODY$
14 | DECLARE currentTime timestamptz;
15 |
16 | BEGIN
17 | SET TIME ZONE UTC;
18 | currentTime := NOW();
19 |
20 | RETURN QUERY
21 | INSERT INTO events.subscription(resourcefilter, sourcefilter, subjectfilter, typefilter, consumer, endpointurl, createdby, "time", validated)
22 | VALUES ($1, $2, $3, $4, $5, $6, $7, currentTime, $8) RETURNING *;
23 |
24 | END
25 | $BODY$;
26 |
--------------------------------------------------------------------------------
/test/Postman/Platform Events - AT22.postman_environment.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "f6626d0e-e11d-47bd-bb07-b531821bc869",
3 | "name": "Platform Events - AT22",
4 | "values": [
5 | {
6 | "key": "PlatformHostUrl",
7 | "value": "https://platform.at22.altinn.cloud",
8 | "type": "default",
9 | "enabled": true
10 | },
11 | {
12 | "key": "AltinnSlackWebHook",
13 | "value": "",
14 | "type": "secret",
15 | "enabled": true
16 | },
17 | {
18 | "key": "PlatformToken",
19 | "value": "",
20 | "type": "secret",
21 | "enabled": true
22 | },
23 | {
24 | "key": "TestToolsUserPassword",
25 | "value": "",
26 | "type": "secret",
27 | "enabled": true
28 | }
29 | ],
30 | "_postman_variable_scope": "environment",
31 | "_postman_exported_at": "2022-11-18T16:45:51.475Z",
32 | "_postman_exported_using": "Postman/10.3.4"
33 | }
--------------------------------------------------------------------------------
/src/Events/Services/Interfaces/IAppSubscriptionService.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | using Altinn.Platform.Events.Models;
4 |
5 | namespace Altinn.Platform.Events.Services.Interfaces
6 | {
7 | ///
8 | /// Interface for methods related specifically to handling subscriptions for Altinn App event sources
9 | ///
10 | public interface IAppSubscriptionService
11 | {
12 | ///
13 | /// Operation to create a subscription for an Altinn App event source
14 | ///
15 | /// The event subscription
16 | /// A subscription if creation was successful or an error object
17 | public Task<(Subscription Subscription, ServiceError Error)> CreateSubscription(Subscription eventsSubscription);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Events/Services/Interfaces/IGenericSubscriptionService.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | using Altinn.Platform.Events.Models;
4 |
5 | namespace Altinn.Platform.Events.Services.Interfaces
6 | {
7 | ///
8 | /// Interface for methods related specifically to handling subscriptions from generic event sources
9 | ///
10 | public interface IGenericSubscriptionService
11 | {
12 | ///
13 | /// Operation to create a subscription for an Altinn App event source
14 | ///
15 | /// The event subscription
16 | /// A subscription if creation was successful or an error object
17 | public Task<(Subscription Subscription, ServiceError Error)> CreateSubscription(Subscription eventsSubscription);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.32/01-new-indexes.sql:
--------------------------------------------------------------------------------
1 | --- Create B-tree indices on json columns
2 | CREATE INDEX IF NOT EXISTS idx_events_cloudevent_id
3 | ON events.events ((cloudevent ->> 'id'));
4 | CREATE INDEX IF NOT EXISTS idx_events_cloudevent_subject
5 | ON events.events ((cloudevent ->> 'subject'));
6 | CREATE INDEX IF NOT EXISTS idx_events_cloudevent_alternativesubject
7 | ON events.events ((cloudevent ->> 'alternativesubject'));
8 | CREATE INDEX IF NOT EXISTS idx_events_cloudevent_source
9 | ON events.events ((cloudevent ->> 'source'));
10 | CREATE INDEX IF NOT EXISTS idx_events_cloudevent_type
11 | ON events.events ((cloudevent ->> 'type'));
12 | CREATE INDEX IF NOT EXISTS idx_events_cloudevent_time
13 | ON events.events ((cloudevent ->> 'time'));
14 |
15 | -- Drop redundant GIN index
16 | DROP INDEX IF EXISTS events.idx_gin_events_computed_cloudevent;
17 |
--------------------------------------------------------------------------------
/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": ["Microsoft.IdentityModel.Tokens"],
9 | "allowedVersions": "<=6.23.0"
10 | }
11 | ],
12 | "customManagers": [
13 | {
14 | "customType": "regex",
15 | "description": "Manage Alpine OS versions in container image tags",
16 | "managerFilePatterns": [
17 | "/Dockerfile/"
18 | ],
19 | "matchStrings": [
20 | "(?:FROM\\s+)(?[\\S]+):(?[\\S]+)@(?sha256:[a-f0-9]+)"
21 | ],
22 | "versioningTemplate": "regex:^(?[\\S]*\\d+\\.\\d+(?:\\.\\d+)?(?:[\\S]*)?-alpine-?)(?\\d+)\\.(?\\d+)(?:\\.(?\\d+))?$",
23 | "datasourceTemplate": "docker"
24 | }
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.03/02-setup-functions.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.getsubscription(_id integer)
2 | RETURNS TABLE (id bigint, sourcefilter character varying, subjectfilter character varying, typefilter character varying, consumer character varying, endpointurl character varying, createdby character varying, validated BOOLEAN, "time" timestamptz)
3 | LANGUAGE 'plpgsql'
4 |
5 | AS $BODY$
6 | BEGIN
7 | return query
8 | SELECT s.id, s.sourcefilter, s.subjectfilter, s.typefilter, s.consumer, s.endpointurl, s.createdby, s.validated, s."time"
9 | FROM events.subscription s
10 | where s.id = _id;
11 |
12 | END;
13 | $BODY$;
14 |
15 | ------------------
16 | CREATE OR REPLACE PROCEDURE events.deletesubscription(_id integer)
17 | LANGUAGE 'plpgsql'
18 |
19 | AS $BODY$
20 | BEGIN
21 | DELETE
22 | FROM events.subscription s
23 | where s.id = _id;
24 |
25 | END;
26 | $BODY$;
27 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.09/01-setup-function.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.insertevent(
2 | id character varying,
3 | source character varying,
4 | subject character varying,
5 | type character varying,
6 | cloudevent INOUT text)
7 | LANGUAGE 'plpgsql'
8 |
9 | AS $BODY$
10 | DECLARE currentTime timestamptz;
11 | DECLARE currentTimeString character varying;
12 | DECLARE finalCloudEvent text;
13 |
14 | BEGIN
15 | SET TIME ZONE UTC;
16 | currentTime := NOW();
17 | currentTimeString := to_char(currentTime, 'YYYY-MM-DD"T"HH24:MI:SS.USOF');
18 |
19 | finalCloudEvent:= substring($5 from 1 for length($5) -1) || ',"time": "' || currentTimeString || '"}';
20 |
21 | INSERT INTO events.events(id, source, subject, type, "time", cloudevent)
22 | VALUES ($1, $2, $3, $4, currentTime, finalCloudEvent);
23 |
24 | SELECT finalCloudEvent
25 | INTO cloudevent;
26 |
27 | END;
28 | $BODY$;
29 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.10/01-setup-functions.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.getsubscriptionsbyconsumer(
2 | _consumer character varying,
3 | _includeInvalid boolean)
4 | RETURNS TABLE(id bigint, sourcefilter character varying, subjectfilter character varying, typefilter character varying, consumer character varying, endpointurl character varying, createdby character varying, validated boolean, "time" timestamp with time zone)
5 | LANGUAGE 'plpgsql'
6 | COST 100
7 | VOLATILE PARALLEL UNSAFE
8 | ROWS 1000
9 |
10 | AS $BODY$
11 |
12 | BEGIN
13 | return query
14 | SELECT s.id, s.sourcefilter, s.subjectfilter, s.typefilter, s.consumer, s.endpointurl, s.createdby, s.validated, s."time"
15 | FROM events.subscription s
16 | WHERE s.consumer LIKE _consumer
17 | AND s.validated =
18 | CASE WHEN _includeInvalid THEN
19 | false or s.validated = true
20 | ELSE
21 | true
22 | END;
23 |
24 |
25 | END
26 | $BODY$;
--------------------------------------------------------------------------------
/src/Events/Health/HealthCheck.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 | using Microsoft.Extensions.Diagnostics.HealthChecks;
4 |
5 | namespace Altinn.Platform.Events.Health
6 | {
7 | ///
8 | /// Health check service configured in startup
9 | /// Listen to
10 | ///
11 | public class HealthCheck : IHealthCheck
12 | {
13 | ///
14 | /// Verifies the healht status
15 | ///
16 | /// The healtcheck context
17 | /// The cancellationtoken
18 | ///
19 | public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
20 | {
21 | return Task.FromResult(
22 | HealthCheckResult.Healthy("A healthy result."));
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Events/Migration/FunctionsAndProcedures/getsubscriptionsbyconsumer.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.getsubscriptionsbyconsumer_v2(
2 | _consumer character varying,
3 | _includeinvalid boolean)
4 | RETURNS TABLE(id bigint, resourcefilter character varying, sourcefilter character varying, subjectfilter character varying, typefilter character varying, consumer character varying, endpointurl character varying, createdby character varying, validated boolean, "time" timestamp with time zone)
5 | LANGUAGE 'plpgsql'
6 | AS $BODY$
7 |
8 |
9 | BEGIN
10 | return query
11 | SELECT s.id, s.resourcefilter, s.sourcefilter, s.subjectfilter, s.typefilter, s.consumer, s.endpointurl, s.createdby, s.validated, s."time"
12 | FROM events.subscription s
13 | WHERE s.consumer LIKE _consumer
14 | AND s.validated =
15 | CASE WHEN _includeInvalid THEN
16 | false or s.validated = true
17 | ELSE
18 | true
19 | END;
20 |
21 |
22 | END
23 | $BODY$;
--------------------------------------------------------------------------------
/src/Events/Migration/FunctionsAndProcedures/getsubscriptions.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.getsubscriptions(
2 | resource character varying,
3 | subject character varying,
4 | type character varying)
5 | RETURNS TABLE(id bigint, resourcefilter character varying, sourcefilter character varying, subjectfilter character varying, typefilter character varying, consumer character varying, endpointurl character varying, createdby character varying, validated boolean, "time" timestamp with time zone)
6 | LANGUAGE 'plpgsql'
7 |
8 | AS $BODY$
9 | BEGIN
10 | return query
11 | SELECT s.id, s.resourcefilter, s.sourcefilter, s.subjectfilter, s.typefilter, s.consumer, s.endpointurl, s.createdby, s.validated, s."time"
12 | FROM events.subscription s
13 | WHERE s.resourcefilter = resource
14 | AND (s.subjectfilter is NULL OR s.subjectfilter = subject)
15 | AND (s.typefilter is NULL OR s.typefilter = type)
16 | AND s.validated;
17 |
18 | END;
19 | $BODY$;
20 |
--------------------------------------------------------------------------------
/.github/workflows/send-slack-warning.yml:
--------------------------------------------------------------------------------
1 | name: Send Slack Warning
2 |
3 | on:
4 | workflow_call:
5 | inputs:
6 | warning-message:
7 | required: true
8 | type: string
9 | description: 'The warning message to include in the Slack notification'
10 | secrets:
11 | SLACK_WEBHOOK_URL:
12 | required: true
13 |
14 | jobs:
15 | slack-notify:
16 | runs-on: ubuntu-latest
17 | permissions: {}
18 | steps:
19 | - name: Send notification to Slack
20 | uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
21 | with:
22 | webhook-type: incoming-webhook
23 | webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
24 | payload: |
25 | {
26 | "text": ":warning: ${{ inputs.warning-message }} :warning: \n\n Workflow available here: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
27 | }
28 |
--------------------------------------------------------------------------------
/src/Events.Functions/EventsInbound.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Altinn.Platform.Events.Functions.Clients.Interfaces;
3 | using Altinn.Platform.Events.Functions.Extensions;
4 | using CloudNative.CloudEvents;
5 | using Microsoft.Azure.Functions.Worker;
6 |
7 | namespace Altinn.Platform.Events.Functions;
8 |
9 | ///
10 | /// Initializes a new instance of the class.
11 | ///
12 | public class EventsInbound(IEventsClient eventsClient)
13 | {
14 | private readonly IEventsClient _eventsClient = eventsClient;
15 |
16 | ///
17 | /// Retrieves messages from events-inbound queue and push events controller
18 | ///
19 | [Function(nameof(EventsInbound))]
20 | public async Task Run([QueueTrigger("events-inbound", Connection = "QueueStorage")] string item)
21 | {
22 | CloudEvent cloudEvent = item.DeserializeToCloudEvent();
23 | await _eventsClient.PostOutbound(cloudEvent);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.01/01-setup-functions.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.get(
2 | _subject character varying,
3 | _after character varying,
4 | _from timestamp with time zone,
5 | _to timestamp with time zone,
6 | _type text[],
7 | _source text[])
8 | RETURNS TABLE(cloudevents text)
9 | LANGUAGE 'plpgsql'
10 |
11 | AS $BODY$
12 | BEGIN
13 | return query
14 | Select events.events.cloudevent
15 | from events.events
16 | WHERE (_subject = '' OR events.subject = _subject)
17 | AND (_from IS NULL OR events.time >= _from)
18 | AND (_to IS NULL OR events.time <= _to)
19 | AND (_type IS NULL OR events.type ILIKE ANY(_type) )
20 | AND (_source IS NULL OR events.source ILIKE ANY(_source))
21 | AND (_after = '' OR events.sequenceno >(
22 | SELECT
23 | case count(*)
24 | when 0
25 | then 0
26 | else
27 | (SELECT sequenceno
28 | FROM events.events
29 | WHERE id = _after)
30 | end
31 | FROM events.events
32 | WHERE id = _after));
33 | END;
34 | $BODY$;
35 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Mocks/DelegatingHandlerStub.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using System.Net.Http;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 |
7 | namespace Altinn.Platform.Events.Tests.Mocks;
8 |
9 | public class DelegatingHandlerStub : DelegatingHandler
10 | {
11 | private readonly Func> _handlerFunc;
12 |
13 | public DelegatingHandlerStub()
14 | {
15 | _handlerFunc = (request, cancellationToken) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
16 | }
17 |
18 | public DelegatingHandlerStub(Func> handlerFunc)
19 | {
20 | _handlerFunc = handlerFunc;
21 | }
22 |
23 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
24 | {
25 | return _handlerFunc(request, cancellationToken);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/test/k6/src/api/app-events.js:
--------------------------------------------------------------------------------
1 | import http from "k6/http";
2 | import * as apiHelper from "../apiHelpers.js";
3 | import * as config from "../config.js";
4 |
5 | export function getEventsForOrg(org, app, queryParams, token) {
6 | var endpoint = config.platformEvents.app + org + "/" + app;
7 |
8 | return getAppEvents(endpoint, queryParams, token);
9 | }
10 |
11 | export function getEventsForParty(queryParams, token) {
12 | var endpoint = config.platformEvents.app + "party";
13 |
14 | return getAppEvents(endpoint, queryParams, token);
15 | }
16 |
17 | export function getEventsFromNextLink(nextLink, token){
18 | return getAppEvents(nextLink, null, token);
19 | }
20 |
21 | function getAppEvents(endpoint, queryParams, token) {
22 | endpoint +=
23 | queryParams != null
24 | ? apiHelper.buildQueryParametersForEndpoint(queryParams)
25 | : "";
26 |
27 | var params = apiHelper.buildHeaderWithBearer(token);
28 |
29 | var response = http.get(endpoint, params);
30 |
31 | return response;
32 | }
33 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.34/01-create-function.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.getsubscriptions(
2 | resource character varying,
3 | subject character varying,
4 | type character varying)
5 | RETURNS TABLE(id bigint, resourcefilter character varying, sourcefilter character varying, subjectfilter character varying, typefilter character varying, consumer character varying, endpointurl character varying, createdby character varying, validated boolean, "time" timestamp with time zone)
6 | LANGUAGE 'plpgsql'
7 | COST 100
8 | VOLATILE PARALLEL UNSAFE
9 | ROWS 1000
10 |
11 | AS $BODY$
12 |
13 |
14 | BEGIN
15 | return query
16 | SELECT s.id, s.resourcefilter, s.sourcefilter, s.subjectfilter, s.typefilter, s.consumer, s.endpointurl, s.createdby, s.validated, s."time"
17 | FROM events.subscription s
18 | WHERE s.resourcefilter = resource
19 | AND (s.subjectfilter is NULL OR s.subjectfilter = subject)
20 | AND (s.typefilter is NULL OR s.typefilter = type)
21 | AND s.validated;
22 |
23 | END;
24 | $BODY$;
25 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.14/01-setup-functions.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.getsubscriptions(
2 | source character varying,
3 | subject character varying,
4 | type character varying)
5 | RETURNS TABLE(
6 | id bigint,
7 | sourcefilter character varying,
8 | subjectfilter character varying,
9 | typefilter character varying,
10 | consumer character varying,
11 | endpointurl character varying,
12 | createdby character varying,
13 | validated boolean,
14 | "time" timestamp with time zone)
15 | LANGUAGE 'plpgsql'
16 | COST 100
17 | VOLATILE PARALLEL UNSAFE
18 | ROWS 1000
19 |
20 | AS $BODY$
21 |
22 | BEGIN
23 | return query
24 | SELECT s.id, s.sourcefilter, s.subjectfilter, s.typefilter, s.consumer, s.endpointurl, s.createdby, s.validated, s."time"
25 | FROM events.subscription s
26 | WHERE (s.subjectfilter is NULL OR s.subjectfilter = subject)
27 | AND s.sourcefilter = source
28 | AND (s.typefilter is NULL OR s.typefilter = type)
29 | AND s.validated;
30 |
31 | END;
32 | $BODY$;
33 |
--------------------------------------------------------------------------------
/src/Events/Services/Interfaces/IOutboundService.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 |
4 | using CloudNative.CloudEvents;
5 |
6 | namespace Altinn.Platform.Events.Services.Interfaces;
7 |
8 | ///
9 | /// Represents the requirements of an outbound sericve implementation with the capability to
10 | /// identify subscriptions and queue events for delivery.
11 | ///
12 | public interface IOutboundService
13 | {
14 | ///
15 | /// Finds subscriptions that match the given event and queues the event for delivery to those subscriptions.
16 | ///
17 | /// The CloudEvent to be processed.
18 | ///
19 | /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
20 | ///
21 | /// A task representing the asynchronous operation.
22 | Task PostOutbound(CloudEvent cloudEvent, CancellationToken cancellationToken);
23 | }
24 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.02/01-setup-functions.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.get(
2 | _subject character varying,
3 | _after character varying,
4 | _from timestamp with time zone,
5 | _to timestamp with time zone,
6 | _type text[],
7 | _source text[])
8 | RETURNS TABLE(cloudevents text)
9 | LANGUAGE 'plpgsql'
10 |
11 | AS $BODY$
12 | BEGIN
13 | return query
14 | SELECT events.events.cloudevent
15 | FROM events.events
16 | WHERE (_subject = '' OR events.subject = _subject)
17 | AND (_from IS NULL OR events.time >= _from)
18 | AND (_to IS NULL OR events.time <= _to)
19 | AND (_type IS NULL OR events.type ILIKE ANY(_type) )
20 | AND (_source IS NULL OR events.source ILIKE ANY(_source))
21 | AND (_after = '' OR events.sequenceno >(
22 | SELECT
23 | case count(*)
24 | when 0
25 | then 0
26 | else
27 | (SELECT sequenceno
28 | FROM events.events
29 | WHERE id = _after)
30 | end
31 | FROM events.events
32 | WHERE id = _after))
33 | ORDER BY events.sequenceno;
34 | END;
35 | $BODY$;
36 |
--------------------------------------------------------------------------------
/src/Events/Middleware/EnableRequestBodyBufferingMiddleware.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Microsoft.AspNetCore.Http;
3 |
4 | namespace Altinn.Platform.Events.Middleware
5 | {
6 | ///
7 | /// Enable buffering when using raw request body extraction
8 | ///
9 | public class EnableRequestBodyBufferingMiddleware
10 | {
11 | private readonly RequestDelegate _next;
12 |
13 | ///
14 | /// Set delegate
15 | ///
16 | /// Next delegate
17 | public EnableRequestBodyBufferingMiddleware(RequestDelegate next) =>
18 | _next = next;
19 |
20 | ///
21 | /// InvokeAsync
22 | ///
23 | /// HttpContext
24 | ///
25 | public async Task InvokeAsync(HttpContext context)
26 | {
27 | context.Request.EnableBuffering();
28 |
29 | await _next(context);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.11/01-setup-functions.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.get(
2 | _subject character varying,
3 | _after character varying,
4 | _from timestamp with time zone,
5 | _to timestamp with time zone,
6 | _type text[],
7 | _source text[],
8 | _size int)
9 | RETURNS TABLE(cloudevents text)
10 | LANGUAGE 'plpgsql'
11 |
12 | AS $BODY$
13 | BEGIN
14 | return query
15 | SELECT events.events.cloudevent
16 | FROM events.events
17 | WHERE (_subject = '' OR events.subject = _subject)
18 | AND (_from IS NULL OR events.time >= _from)
19 | AND (_to IS NULL OR events.time <= _to)
20 | AND (_type IS NULL OR events.type ILIKE ANY(_type) )
21 | AND (_source IS NULL OR events.source ILIKE ANY(_source))
22 | AND (_after = '' OR events.sequenceno >(
23 | SELECT
24 | case count(*)
25 | when 0
26 | then 0
27 | else
28 | (SELECT sequenceno
29 | FROM events.events
30 | WHERE id = _after)
31 | end
32 | FROM events.events
33 | WHERE id = _after))
34 | ORDER BY events.sequenceno
35 | limit _size;
36 | END;
37 | $BODY$;
38 |
--------------------------------------------------------------------------------
/src/Events/Controllers/ErrorController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Authorization;
2 | using Microsoft.AspNetCore.Mvc;
3 |
4 | namespace Altinn.Platform.Events.Controllers
5 | {
6 | ///
7 | /// Handles the presentation of unhandled exceptions during the execution of a request.
8 | ///
9 | [ApiController]
10 | [ApiExplorerSettings(IgnoreApi = true)]
11 | [AllowAnonymous]
12 | [Route("events/api/v1")]
13 | public class ErrorController : ControllerBase
14 | {
15 | ///
16 | /// Create a response with a new instance with limited information.
17 | ///
18 | ///
19 | /// This method cannot be called directly. It is used by the API framework as a way to output ProblemDetails
20 | /// if there has been an unhandled exception.
21 | ///
22 | /// A new instance.
23 | [Route("error")]
24 | public IActionResult Error() => Problem();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.25/01-alter-function.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.getevents(
2 | _subject character varying,
3 | _after character varying,
4 | _type text[],
5 | _source text[],
6 | _size integer)
7 | RETURNS TABLE(cloudevents text)
8 | LANGUAGE 'plpgsql'
9 | COST 100
10 | VOLATILE PARALLEL UNSAFE
11 | ROWS 1000
12 |
13 | AS $BODY$
14 | BEGIN
15 | return query
16 | SELECT cast(cloudevent as text) as cloudevents
17 | FROM events.events
18 | WHERE (_subject IS NULL OR cloudevent->>'subject' = _subject)
19 | AND (_type IS NULL OR cloudevent->>'type' ILIKE ANY(_type) )
20 | AND (_source IS NULL OR cloudevent->>'source' ILIKE ANY(_source))
21 | AND (_after = '' OR sequenceno >(
22 | SELECT
23 | case count(*)
24 | when 0
25 | then 0
26 | else
27 | (SELECT sequenceno
28 | FROM events.events
29 | WHERE cloudevent->>'id' = _after
30 | ORDER BY sequenceno ASC
31 | LIMIT 1)
32 | end
33 | FROM events.events
34 | WHERE cloudevent->>'id' = _after))
35 | ORDER BY sequenceno
36 | limit _size;
37 | END;
38 | $BODY$;
39 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/XacmlJsonResponse/permit_publish_one.json:
--------------------------------------------------------------------------------
1 | {
2 | "Response": [
3 | {
4 | "Decision": "Permit",
5 | "Status": {
6 | "StatusMessage": null,
7 | "StatusDetails": null,
8 | "StatusCode": {
9 | "Value": "urn:oasis:names:tc:xacml:1.0:status:ok",
10 | "StatusCode": null
11 | }
12 | },
13 | "Obligations": null,
14 | "AssociateAdvice": null,
15 | "Category": [
16 | {
17 | "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource",
18 | "Id": null,
19 | "Content": null,
20 | "Attribute": [
21 | {
22 | "AttributeId": "urn:altinn:event-id",
23 | "Value": "e7c581bc-e931-46c8-bfc0-3c6716d8da15",
24 | "Issuer": null,
25 | "DataType": "http://www.w3.org/2001/XMLSchema#string",
26 | "IncludeInResult": false
27 | }
28 | ]
29 | }
30 | ],
31 | "PolicyIdentifierList": null
32 | }
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/XacmlJsonResponse/permit_subscribe_one.json:
--------------------------------------------------------------------------------
1 | {
2 | "Response": [
3 | {
4 | "Decision": "Permit",
5 | "Status": {
6 | "StatusMessage": null,
7 | "StatusDetails": null,
8 | "StatusCode": {
9 | "Value": "urn:oasis:names:tc:xacml:1.0:status:ok",
10 | "StatusCode": null
11 | }
12 | },
13 | "Obligations": null,
14 | "AssociateAdvice": null,
15 | "Category": [
16 | {
17 | "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource",
18 | "Id": null,
19 | "Content": null,
20 | "Attribute": [
21 | {
22 | "AttributeId": "urn:altinn:event-id",
23 | "Value": "e7c581bc-e931-46c8-bfc0-3c6716d8da15",
24 | "Issuer": null,
25 | "DataType": "http://www.w3.org/2001/XMLSchema#string",
26 | "IncludeInResult": false
27 | }
28 | ]
29 | }
30 | ],
31 | "PolicyIdentifierList": null
32 | }
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Mocks/PublicSigningKeyProviderMock.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Security.Cryptography.X509Certificates;
5 | using System.Threading.Tasks;
6 |
7 | using Altinn.Common.AccessToken.Services;
8 |
9 | using Microsoft.IdentityModel.Tokens;
10 |
11 | namespace Altinn.Platform.Events.Tests.Mocks
12 | {
13 | public class PublicSigningKeyProviderMock : IPublicSigningKeyProvider
14 | {
15 | public SigningCredentials GetSigningCredentials()
16 | {
17 | throw new NotImplementedException();
18 | }
19 |
20 | public Task> GetSigningKeys(string issuer)
21 | {
22 | List signingKeys = new List();
23 |
24 | X509Certificate2 cert = X509CertificateLoader.LoadCertificateFromFile($"{issuer}-org.pem");
25 | SecurityKey key = new X509SecurityKey(cert);
26 |
27 | signingKeys.Add(key);
28 |
29 | return Task.FromResult(signingKeys.AsEnumerable());
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Events.Functions/Models/Payloads/SlackEnvelope.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 |
5 | using Altinn.Platform.Events.Functions.Extensions;
6 |
7 | using CloudNative.CloudEvents;
8 |
9 | namespace Altinn.Platform.Events.Functions.Models.Payloads
10 | {
11 | ///
12 | /// Represents a model for sending to Slack.
13 | ///
14 | public class SlackEnvelope
15 | {
16 | ///
17 | /// Gets or sets the cloudevent as string.
18 | ///
19 | [JsonPropertyName("text")]
20 | public CloudEvent CloudEvent { get; set; }
21 |
22 | ///
23 | /// Serializes the SlackEnvelope to a JSON string.
24 | ///
25 | /// Serialized slack envelope
26 | public string Serialize()
27 | {
28 | string serializedCloudEvent = CloudEvent.Serialize().Replace("\"", "\\\"");
29 | return CloudEvent == null ? "{ }" : string.Format("{{\"text\": \"{0}\"}}", serializedCloudEvent);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Events/Migration/ReadMe.txt:
--------------------------------------------------------------------------------
1 | Usage/rules:
2 |
3 | - The tool DbTools is used to autogenerate a Yuniql deploy file for functions and procedures. Other artifacts
4 | like tables and index must currently be maintained manually.
5 |
6 | - Each function/proc has a separate file in the Migration/FunctionsAndProcedures folder.
7 |
8 | - After build the tool DbTools.exe is automatically run to generate vx.xx/ZZ-functions-and-procedures.sql
9 | based on the content in all files in the FunctionsAndProcedures folder. ZZ is assumed to be 01 if no other
10 | file is found with another number. (Makes it possible to deploy other stuff before procs/funcs.)
11 |
12 | - The file name of a proc/func should be the base proc/function name without any version postfix.
13 | The same filename is kept if the proc/func gets a new version.
14 |
15 | - Any drop commands must be coded at the top of the func/proc file or in a separate file in the related v.x.xx folder.
16 |
17 | - A new vx.xx folder must be created when a func/proc is created/updated after last deploy. If not the current
18 | vx.xx will be used, and the migration will not be executed by yuniql.
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 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 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Models/EventsTableEntry.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.Json.Serialization;
3 |
4 | using CloudNative.CloudEvents;
5 |
6 | namespace Altinn.Platform.Events.Tests.Models
7 | {
8 | public class EventsTableEntry
9 | {
10 | [JsonPropertyName("id")]
11 | public string Id { get; set; }
12 |
13 | [JsonPropertyName("resource")]
14 | public string Resource { get; set; }
15 |
16 | [JsonPropertyName("source")]
17 | public Uri Source { get; set; }
18 |
19 | [JsonPropertyName("type")]
20 | public string Type { get; set; }
21 |
22 | [JsonPropertyName("subject")]
23 | public string Subject { get; set; }
24 |
25 | [JsonPropertyName("alternativesubject")]
26 | public string AlternativeSubject { get; set; }
27 |
28 | [JsonPropertyName("time")]
29 | public DateTime? Time { get; set; }
30 |
31 | [JsonPropertyName("sequenceno")]
32 | public int SequenceNo { get; set; }
33 |
34 | [JsonPropertyName("cloudEvent")]
35 | public CloudEvent CloudEvent { get; set; }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Events/Migration/FunctionsAndProcedures/createlogspartition.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.create_logs_partition(
2 | )
3 | RETURNS integer
4 | LANGUAGE 'plpgsql'
5 | COST 100
6 | VOLATILE PARALLEL UNSAFE
7 | AS $BODY$
8 | DECLARE
9 | thisMonth int;
10 | nextMonth int;
11 | thisYear int;
12 | nextYear int;
13 | fromMonth varchar;
14 | toMonth varchar;
15 | partitionName varchar;
16 | BEGIN
17 | SELECT (EXTRACT(MONTH from current_date)) INTO thisMonth;
18 | SELECT (EXTRACT(YEAR from current_date)) INTO thisYear;
19 |
20 | nextYear = thisYear;
21 | nextMonth = thisMonth + 1;
22 |
23 | IF nextMonth = 13 THEN
24 | nextMonth = 1;
25 | nextYear = nextYear + 1;
26 | END IF;
27 |
28 | fromMonth = thisYear || '-' || lpad(thisMonth::varchar, 2, '0') || '-01' ;
29 | toMonth = nextYear || '-' || lpad(nextMonth::varchar, 2, '0') || '-01';
30 | partitionName = 'events.trace_log_y' || nextYear || 'm' || nextMonth;
31 |
32 | EXECUTE
33 | format('CREATE TABLE %s PARTITION OF events.trace_log FOR VALUES FROM (''%s'') TO (''%s'')', partitionName, fromMonth, toMonth);
34 |
35 | RETURN 1;
36 | END;
37 | $BODY$;
38 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # General setting that applies Git's binary detection for file-types not specified below
2 | # Meaning, for 'text-guessed' files:
3 | # use normalization (convert crlf -> lf on commit, i.e. use `text` setting)
4 | # & do unspecified diff behavior (if file content is recognized as text & filesize < core.bigFileThreshold, do text diff on file changes)
5 | * text=auto
6 |
7 |
8 | # Override with explicit specific settings for known and/or likely text files in our repo that should be normalized
9 | # where diff{=optional_pattern} means "do text diff {with specific text pattern} and -diff means "don't do text diffs".
10 | # Unspecified diff behavior is decribed above
11 | *.cer text -diff
12 | *.cmd text
13 | *.cs text diff=csharp
14 | *.csproj text
15 | *.css text diff=css
16 | Dockerfile text
17 | *.json text
18 | *.md text diff=markdown
19 | *.msbuild text
20 | *.pem text -diff
21 | *.ps1 text
22 | *.sln text
23 | *.yaml text
24 | *.yml text
25 |
26 | # Files that should be treated as binary ('binary' is a macro for '-text -diff', i.e. "don't normalize or do text diff on content")
27 | *.jpeg binary
28 | *.pfx binary
29 | *.png binary
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/ttd-org.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDAzCCAeugAwIBAgIJANTdO8o3I8x5MA0GCSqGSIb3DQEBCwUAMA4xDDAKBgNV
3 | BAMTA3R0ZDAeFw0yMDA1MjUxMjIxMzdaFw0zMDA1MjQxMjIxMzdaMA4xDDAKBgNV
4 | BAMTA3R0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMcfTsXwwLyC
5 | UkIz06eadWJvG3yrzT+ZB2Oy/WPaZosDnPcnZvCDueN+oy0zTx5TyH5gCi1FvzX2
6 | 7G2eZEKwQaRPv0yuM+McHy1rXxMSOlH/ebP9KJj3FDMUgZl1DCAjJxSAANdTwdrq
7 | ydVg1Crp37AQx8IIEjnBhXsfQh1uPGt1XwgeNyjl00IejxvQOPzd1CofYWwODVtQ
8 | l3PKn1SEgOGcB6wuHNRlnZPCIelQmqxWkcEZiu/NU+kst3NspVUQG2Jf2AF8UWgC
9 | rnrhMQR0Ra1Vi7bWpu6QIKYkN9q0NRHeRSsELOvTh1FgDySYJtNd2xDRSf6IvOiu
10 | tSipl1NZlV0CAwEAAaNkMGIwIAYDVR0OAQH/BBYEFIwq/KbSMzLETdo9NNxj0rz4
11 | qMqVMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgWgMCAGA1UdJQEB/wQWMBQG
12 | CCsGAQUFBwMBBggrBgEFBQcDAjANBgkqhkiG9w0BAQsFAAOCAQEAE56UmH5gEYbe
13 | 1kVw7nrfH0R9FyVZGeQQWBn4/6Ifn+eMS9mxqe0Lq74Ue1zEzvRhRRqWYi9JlKNf
14 | 7QQNrc+DzCceIa1U6cMXgXKuXquVHLmRfqvKHbWHJfIkaY8Mlfy++77UmbkvIzly
15 | T1HVhKKp6Xx0r5koa6frBh4Xo/vKBlEyQxWLWF0RPGpGErnYIosJ41M3Po3nw3lY
16 | f7lmH47cdXatcntj2Ho/b2wGi9+W29teVCDfHn2/0oqc7K0EOY9c2ODLjUvQyPZR
17 | OD2yykpyh9x/YeYHFDYdLDJ76/kIdxN43kLU4/hTrh9tMb1PZF+/4DshpAlRoQuL
18 | o8I8avQm/A==
19 | -----END CERTIFICATE-----
20 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Roles/User_1337/party_1337/roles.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "Type": "altinn",
4 | "value": "PRIV"
5 | },
6 | {
7 | "Type": "altinn",
8 | "value": "UTINN"
9 | },
10 | {
11 | "Type": "altinn",
12 | "value": "LOPER"
13 | }
14 | ,
15 | {
16 | "Type": "altinn",
17 | "value": "ADMAI"
18 | },
19 | {
20 | "Type": "altinn",
21 | "value": "PRIUT"
22 | },
23 | {
24 | "Type": "altinn",
25 | "value": "REGNA"
26 | },
27 | {
28 | "Type": "altinn",
29 | "value": "SISKD"
30 | },
31 | {
32 | "Type": "altinn",
33 | "value": "UILUF"
34 | },
35 | {
36 | "Type": "altinn",
37 | "value": "UTOMR"
38 | },
39 | {
40 | "Type": "altinn",
41 | "value": "PAVAD"
42 | },
43 | {
44 | "Type": "altinn",
45 | "value": "KOMAB"
46 | },
47 | {
48 | "Type": "altinn",
49 | "value": "BOADM"
50 | },
51 | {
52 | "Type": "altinn",
53 | "value": "A0212"
54 | },
55 | {
56 | "Type": "altinn",
57 | "value": "A0236"
58 | },
59 | {
60 | "Type": "altinn",
61 | "value": "A0278"
62 | },
63 | {
64 | "Type": "altinn",
65 | "value": "A0282"
66 | }
67 | ]
68 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/TestingUtils/XacmlMapperHelperTests.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | using Altinn.Authorization.ABAC.Xacml.JsonProfile;
4 | using Altinn.Platform.Events.Authorization;
5 |
6 | using Xunit;
7 |
8 | namespace Altinn.Platform.Events.Tests.TestingUtils;
9 |
10 | public class XacmlMapperHelperTests
11 | {
12 | [Theory]
13 | [InlineData("/user/342345", "urn:altinn:userid", "342345")]
14 | [InlineData("/org/ttd", "urn:altinn:org", "ttd")]
15 | [InlineData("/party/532345", "urn:altinn:partyid", "532345")]
16 | [InlineData("/organisation/876765454", "urn:altinn:organization:identifier-no", "876765454")]
17 | [InlineData("/systemuser/systemUserName", "urn:altinn:systemuser:uuid", "systemUserName")]
18 | public void CreateSubjectAttributes_Assert_correct_attribute_id_and_value(string subject, string attributId, string value)
19 | {
20 | // Act
21 | XacmlJsonCategory actual = new XacmlJsonCategory().AddSubjectAttribute(subject);
22 |
23 | // Assert
24 | Assert.Single(actual.Attribute);
25 | Assert.Equal(attributId, actual.Attribute[0].AttributeId);
26 | Assert.Equal(value, actual.Attribute[0].Value);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/platform-org.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDAzCCAeugAwIBAgIJANTdO8o3I8x5MA0GCSqGSIb3DQEBCwUAMA4xDDAKBgNV
3 | BAMTA3R0ZDAeFw0yMDA1MjUxMjIxMzdaFw0zMDA1MjQxMjIxMzdaMA4xDDAKBgNV
4 | BAMTA3R0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMcfTsXwwLyC
5 | UkIz06eadWJvG3yrzT+ZB2Oy/WPaZosDnPcnZvCDueN+oy0zTx5TyH5gCi1FvzX2
6 | 7G2eZEKwQaRPv0yuM+McHy1rXxMSOlH/ebP9KJj3FDMUgZl1DCAjJxSAANdTwdrq
7 | ydVg1Crp37AQx8IIEjnBhXsfQh1uPGt1XwgeNyjl00IejxvQOPzd1CofYWwODVtQ
8 | l3PKn1SEgOGcB6wuHNRlnZPCIelQmqxWkcEZiu/NU+kst3NspVUQG2Jf2AF8UWgC
9 | rnrhMQR0Ra1Vi7bWpu6QIKYkN9q0NRHeRSsELOvTh1FgDySYJtNd2xDRSf6IvOiu
10 | tSipl1NZlV0CAwEAAaNkMGIwIAYDVR0OAQH/BBYEFIwq/KbSMzLETdo9NNxj0rz4
11 | qMqVMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgWgMCAGA1UdJQEB/wQWMBQG
12 | CCsGAQUFBwMBBggrBgEFBQcDAjANBgkqhkiG9w0BAQsFAAOCAQEAE56UmH5gEYbe
13 | 1kVw7nrfH0R9FyVZGeQQWBn4/6Ifn+eMS9mxqe0Lq74Ue1zEzvRhRRqWYi9JlKNf
14 | 7QQNrc+DzCceIa1U6cMXgXKuXquVHLmRfqvKHbWHJfIkaY8Mlfy++77UmbkvIzly
15 | T1HVhKKp6Xx0r5koa6frBh4Xo/vKBlEyQxWLWF0RPGpGErnYIosJ41M3Po3nw3lY
16 | f7lmH47cdXatcntj2Ho/b2wGi9+W29teVCDfHn2/0oqc7K0EOY9c2ODLjUvQyPZR
17 | OD2yykpyh9x/YeYHFDYdLDJ76/kIdxN43kLU4/hTrh9tMb1PZF+/4DshpAlRoQuL
18 | o8I8avQm/A==
19 | -----END CERTIFICATE-----
20 |
--------------------------------------------------------------------------------
/src/Events/Models/ServiceError.cs:
--------------------------------------------------------------------------------
1 | namespace Altinn.Platform.Events.Models
2 | {
3 | ///
4 | /// A class representing a service error object used to transfere error information from service to controller.
5 | ///
6 | public class ServiceError
7 | {
8 | ///
9 | /// The error code
10 | ///
11 | /// An error code translates directly into an HTTP status code
12 | public int ErrorCode { get; private set; }
13 |
14 | ///
15 | /// The error message
16 | ///
17 | public string ErrorMessage { get; private set; }
18 |
19 | ///
20 | /// Create a new instance of a service error
21 | ///
22 | public ServiceError(int errorCode, string errorMessage)
23 | {
24 | ErrorCode = errorCode;
25 | ErrorMessage = errorMessage;
26 | }
27 |
28 | ///
29 | /// Create a new instance of a service error
30 | ///
31 | public ServiceError(int errorCode)
32 | {
33 | ErrorCode = errorCode;
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.26/01-alter-function.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.getevents(
2 | _subject character varying,
3 | _alternativesubject character varying,
4 | _after character varying,
5 | _type text[],
6 | _source character varying,
7 | _size integer)
8 | RETURNS TABLE(cloudevents text)
9 | LANGUAGE 'plpgsql'
10 | COST 100
11 | VOLATILE PARALLEL UNSAFE
12 | ROWS 1000
13 |
14 | AS $BODY$
15 | BEGIN
16 | return query
17 | SELECT cast(cloudevent as text) as cloudevents
18 | FROM events.events
19 | WHERE (_subject IS NULL OR cloudevent->>'subject' = _subject)
20 | AND (_alternativeSubject IS NULL OR cloudevent->>'alternativesubject' = _alternativesubject)
21 | AND (_source IS NULL OR cloudevent->>'source' ILIKE _source)
22 | AND (_type IS NULL OR cloudevent->>'type' ILIKE ANY(_type) )
23 | AND (_after = '' OR sequenceno >(
24 | SELECT
25 | case count(*)
26 | when 0
27 | then 0
28 | else
29 | (SELECT sequenceno
30 | FROM events.events
31 | WHERE cloudevent->>'id' = _after
32 | ORDER BY sequenceno ASC
33 | LIMIT 1)
34 | end
35 | FROM events.events
36 | WHERE cloudevent->>'id' = _after))
37 | ORDER BY sequenceno
38 | limit _size;
39 | END;
40 | $BODY$;
41 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.20/01-alter-function.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.getappevents(
2 | _subject character varying,
3 | _after character varying,
4 | _from timestamp with time zone,
5 | _to timestamp with time zone,
6 | _type text[],
7 | _source text[],
8 | _size integer)
9 | RETURNS TABLE(cloudevents text)
10 | LANGUAGE 'plpgsql'
11 | COST 100
12 | VOLATILE PARALLEL UNSAFE
13 | ROWS 1000
14 |
15 | AS $BODY$
16 | BEGIN
17 | return query
18 | SELECT events.events_app.cloudevent
19 | FROM events.events_app
20 | WHERE (_subject = '' OR events_app.subject = _subject)
21 | AND (_from IS NULL OR events_app.time >= _from)
22 | AND (_to IS NULL OR events_app.time <= _to)
23 | AND (_type IS NULL OR events_app.type ILIKE ANY(_type) )
24 | AND (_source IS NULL OR events_app.source ILIKE ANY(_source))
25 | AND (_after = '' OR events_app.sequenceno >(
26 | SELECT
27 | case count(*)
28 | when 0
29 | then 0
30 | else
31 | (SELECT sequenceno
32 | FROM events.events_app
33 | WHERE id = _after
34 | ORDER BY sequenceno ASC
35 | LIMIT 1)
36 | end
37 | FROM events.events_app
38 | WHERE id = _after))
39 | ORDER BY events_app.sequenceno
40 | limit _size;
41 | END;
42 | $BODY$;
43 |
--------------------------------------------------------------------------------
/test/k6/src/api/events.js:
--------------------------------------------------------------------------------
1 | import http from "k6/http";
2 |
3 | import * as config from "../config.js";
4 |
5 | import * as apiHelpers from "../apiHelpers.js";
6 |
7 | export function postCloudEvent(serializedCloudEvent, token) {
8 | var endpoint = config.platformEvents.events;
9 |
10 | var params = apiHelpers.buildHeaderWithBearerAndContentType(
11 | token,
12 | "application/cloudevents+json"
13 | );
14 |
15 | var response = http.post(endpoint, serializedCloudEvent, params);
16 |
17 | return response;
18 | }
19 |
20 | export function getCloudEvents(queryParams, token) {
21 | var endpoint = config.platformEvents.events;
22 | return getEvents(endpoint, queryParams, token);
23 | }
24 |
25 | export function getEventsFromNextLink(nextLink, token) {
26 | return getEvents(nextLink, null, token);
27 | }
28 |
29 | function getEvents(endpoint, queryParams, token) {
30 | endpoint +=
31 | queryParams != null
32 | ? apiHelpers.buildQueryParametersForEndpoint(queryParams)
33 | : "";
34 |
35 | var params = apiHelpers.buildHeaderWithBearerAndContentType(
36 | token,
37 | "application/cloudevents+json"
38 | );
39 |
40 | var response = http.get(endpoint, params);
41 |
42 | return response;
43 | }
44 |
45 |
--------------------------------------------------------------------------------
/src/Events/Extensions/UriExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Security.Cryptography;
5 | using System.Text;
6 | using System.Text.RegularExpressions;
7 |
8 | namespace Altinn.Platform.Events.Extensions
9 | {
10 | ///
11 | /// Class for extension methods related to hashing a uri
12 | ///
13 | public static class UriExtensions
14 | {
15 | private static string _urnPattern = @"^urn:[a-z0-9-]{1,30}(:[a-z0-9()+,.\-;$_!\/]{1,100}){1,10}$";
16 |
17 | ///
18 | /// Validates that the provided uri is a urn or an url with the https scheme.
19 | ///
20 | public static bool IsValidUrlOrUrn(Uri uri)
21 | {
22 | return uri.Scheme == "https" || IsValidUrn(uri.ToString());
23 | }
24 |
25 | ///
26 | /// Validates that the provided uri is a urn.
27 | ///
28 | public static bool IsValidUrn(string potentialUrn)
29 | {
30 | return Uri.IsWellFormedUriString(potentialUrn, UriKind.Absolute) && Regex.IsMatch(potentialUrn, _urnPattern, RegexOptions.None, TimeSpan.FromSeconds(0.5));
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/sdk:10.0.101-alpine3.22@sha256:0fba99926e4f12405c78f37941312f00c1aadb178bd63616f5d96fc2af5a26a9 AS build
2 |
3 | COPY src/Events ./Events
4 | COPY src/DbTools ./DbTools
5 | COPY src/Events.Common ./Events.Common
6 | COPY src/Events/Migration ./Migration
7 |
8 | WORKDIR /DbTools
9 | RUN dotnet build ./DbTools.csproj -c Release -o /app_tools
10 |
11 | # Build the Events project
12 | WORKDIR /Events
13 | RUN dotnet build ./Altinn.Platform.Events.csproj -c Release -o /app_output
14 | RUN dotnet publish ./Altinn.Platform.Events.csproj -c Release -o /app_output
15 |
16 | FROM mcr.microsoft.com/dotnet/aspnet:10.0.1-alpine3.23@sha256:900e8fc49fe97a7a8d9134612438d33c5c0e943dae8081b060054093d16e6ab4 AS final
17 |
18 | EXPOSE 5080
19 | WORKDIR /app
20 | COPY --from=build /app_output .
21 |
22 | COPY --from=build /Events/Migration ./Migration
23 |
24 | # setup the user and group
25 | # the user will have no password, using shell /bin/false and using the group dotnet
26 | RUN addgroup -g 3000 dotnet && adduser -u 1000 -G dotnet -D -s /bin/false dotnet
27 | # update permissions of files if neccessary before becoming dotnet user
28 | USER dotnet
29 | RUN mkdir /tmp/logtelemetry
30 |
31 | ENTRYPOINT ["dotnet", "Altinn.Platform.Events.dll"]
32 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Functions.Tests/IntegrationTests/RequiresAzuriteFactAttribute.cs:
--------------------------------------------------------------------------------
1 | using Xunit;
2 |
3 | namespace Altinn.Platform.Events.Functions.Tests.IntegrationTests;
4 |
5 | ///
6 | /// Indicates that a test method requires Azurite to be running locally in order to execute.
7 | ///
8 | /// This attribute is used to conditionally skip tests that depend on Azurite, a local Azure Storage
9 | /// emulator. The test will be skipped if the environment variable ENABLE_AZURITE_TESTS is not set to (case-insensitive) or 1.
11 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
12 | public sealed class RequiresAzuriteFactAttribute : FactAttribute
13 | {
14 | public RequiresAzuriteFactAttribute()
15 | {
16 | if (!AzuriteTestsEnabled())
17 | {
18 | Skip = "Skipped: These tests require Azurite running on a local machine.";
19 | }
20 | }
21 |
22 | private static bool AzuriteTestsEnabled()
23 | {
24 | var v = Environment.GetEnvironmentVariable("ENABLE_AZURITE_TESTS");
25 | return string.Equals(v, "1", StringComparison.Ordinal)
26 | || string.Equals(v, "true", StringComparison.OrdinalIgnoreCase);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Events/Migration/FunctionsAndProcedures/getevents.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.getevents(
2 | _resource character varying,
3 | _subject character varying,
4 | _alternativesubject character varying,
5 | _after character varying,
6 | _type text[],
7 | _size integer)
8 | RETURNS TABLE(cloudevents text)
9 | LANGUAGE 'plpgsql'
10 | AS $BODY$
11 |
12 | DECLARE
13 | _sequenceno bigint;
14 | BEGIN
15 | IF _after IS NOT NULL AND _after <> '' THEN
16 | SELECT
17 | case count(*)
18 | when 0
19 | then 0
20 | else
21 | (SELECT MIN(sequenceno) FROM events.events
22 | WHERE cloudevent->>'id' = _after)
23 | end
24 | INTO _sequenceno
25 | FROM events.events
26 | WHERE cloudevent->>'id' = _after;
27 | END IF;
28 | return query
29 | SELECT cast(cloudevent as text) as cloudevents
30 | FROM events.events
31 | WHERE cloudevent->>'resource' = _resource
32 | AND (_subject IS NULL OR cloudevent->>'subject' = _subject)
33 | AND (_alternativeSubject IS NULL OR cloudevent->>'alternativesubject' = _alternativesubject)
34 | AND (_type IS NULL OR cloudevent->>'type' LIKE ANY(_type) )
35 | AND registeredtime <= now() - interval '30 second'
36 | AND (_after IS NULL OR _after = '' OR sequenceno > _sequenceno)
37 | ORDER BY sequenceno
38 | limit _size;
39 | END;
40 | $BODY$;
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Models/XacmlResourceAttributes.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace Altinn.Platform.Event.Tests.Models
6 | {
7 | public class XacmlResourceAttributes
8 | {
9 | ///
10 | /// Gets or sets the value for org attribute
11 | ///
12 | public string OrgValue { get; set; }
13 |
14 | ///
15 | /// Gets or sets the value for app attribute
16 | ///
17 | public string AppValue { get; set; }
18 |
19 | ///
20 | /// Gets or sets the value for instance attribute
21 | ///
22 | public string InstanceValue { get; set; }
23 |
24 | ///
25 | /// Gets or sets the value for resourceparty attribute
26 | ///
27 | public string ResourcePartyValue { get; set; }
28 |
29 | ///
30 | /// Gets or sets the value for task attribute
31 | ///
32 | public string TaskValue { get; set; }
33 |
34 | ///
35 | /// Gets or sets the value for app resource.
36 | ///
37 | public string AppResourceValue { get; set; }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.13/01-setup-functions.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.find_subscription(
2 | _sourcefilter character varying,
3 | _subjectfilter character varying,
4 | _typefilter character varying,
5 | _consumer character varying,
6 | _endpointurl character varying
7 | )
8 | RETURNS TABLE(
9 | id bigint,
10 | sourcefilter character varying,
11 | subjectfilter character varying,
12 | typefilter character varying,
13 | consumer character varying,
14 | endpointurl character varying,
15 | createdby character varying,
16 | validated boolean,
17 | "time" timestamp with time zone
18 | )
19 | LANGUAGE 'plpgsql'
20 | COST 100
21 | VOLATILE PARALLEL UNSAFE
22 | ROWS 1000
23 |
24 | AS $BODY$
25 |
26 | BEGIN
27 | RETURN query
28 | SELECT
29 | s.id, s.sourcefilter, s.subjectfilter, s.typefilter, s.consumer, s.endpointurl, s.createdby, s.validated, s."time"
30 | FROM
31 | events.subscription s
32 | WHERE
33 | s.sourcefilter = _sourcefilter
34 | AND (_subjectfilter IS NULL OR s.subjectfilter = _subjectfilter)
35 | AND (_typefilter IS NULL OR s.typefilter = _typefilter)
36 | AND s.consumer = _consumer
37 | AND s.endpointurl = _endpointurl;
38 |
39 | END
40 | $BODY$;
--------------------------------------------------------------------------------
/src/Events/Repository/ICloudEventRepository.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 |
5 | using CloudNative.CloudEvents;
6 |
7 | namespace Altinn.Platform.Events.Repository
8 | {
9 | ///
10 | /// This interface describes the public contract of a repository implementation for
11 | ///
12 | public interface ICloudEventRepository
13 | {
14 | ///
15 | /// Creates a cloud event in the repository.
16 | ///
17 | /// The json serialized cloud event
18 | Task CreateEvent(string cloudEvent);
19 |
20 | ///
21 | /// Calls a function to retrieve app cloud events based on query params
22 | ///
23 | Task> GetAppEvents(string after, DateTime? from, DateTime? to, string subject, List source, string resource, List type, int size);
24 |
25 | ///
26 | /// Calls a function to retrieve cloud events based on query params
27 | ///
28 | Task> GetEvents(string resource, string after, string subject, string alternativeSubject, List type, int size);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Events/Authorization/PublishScopeOrAccessTokenRequirement.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | using System;
4 |
5 | using Altinn.Common.AccessToken;
6 | using Altinn.Common.PEP.Authorization;
7 |
8 | namespace Altinn.Platform.Events.Authorization;
9 |
10 | ///
11 | /// This requirement was created to allow access if either Scope or AccessToken verification is successful.
12 | /// It inherits from both and which
13 | /// will trigger both and . If any of them
14 | /// indicate success, authorization will succeed.
15 | ///
16 | public class PublishScopeOrAccessTokenRequirement : IAccessTokenRequirement, IScopeAccessRequirement
17 | {
18 | ///
19 | /// Initializes a new instance of the class with the given scope.
20 | ///
21 | public PublishScopeOrAccessTokenRequirement(string scope)
22 | {
23 | ApprovedIssuers = Array.Empty();
24 | Scope = new string[] { scope };
25 | }
26 |
27 | ///
28 | public string[] ApprovedIssuers { get; set; }
29 |
30 | ///
31 | public string[] Scope { get; set; }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Events/Configuration/PlatformSettings.cs:
--------------------------------------------------------------------------------
1 | namespace Altinn.Platform.Events.Configuration;
2 |
3 | ///
4 | /// Represents a set of configuration options when communicating with the platform API.
5 | /// Instances of this class is initialised with values from app settings. Some values can be overridden by environment variables.
6 | ///
7 | public class PlatformSettings
8 | {
9 | ///
10 | /// Gets or sets the base address for the Register API.
11 | ///
12 | public string RegisterApiBaseAddress { get; set; }
13 |
14 | ///
15 | /// Gets or sets the size of the urn list we send to the Register API for party lookup.
16 | /// Default value is 100.
17 | ///
18 | public int RegisterApiChunkSize { get; set; } = 100;
19 |
20 | ///
21 | /// Gets or sets the url for the Profile API endpoint
22 | ///
23 | public string ApiProfileEndpoint { get; set; }
24 |
25 | ///
26 | /// Gets or sets the apps domain used to match events source
27 | ///
28 | public string AppsDomain { get; set; }
29 |
30 | ///
31 | /// The lifetime to cache subscriptions
32 | ///
33 | public int SubscriptionCachingLifetimeInSeconds { get; set; }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Events/Services/ClaimsPrincipalProvider.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.Security.Claims;
3 |
4 | using Altinn.Platform.Events.Services.Interfaces;
5 |
6 | using Microsoft.AspNetCore.Http;
7 |
8 | namespace Altinn.Platform.Events.Services
9 | {
10 | ///
11 | /// Represents an implementation of using the HttpContext to obtain
12 | /// the current claims principal needed for the application to make calls to other services.
13 | ///
14 | [ExcludeFromCodeCoverage]
15 | public class ClaimsPrincipalProvider : IClaimsPrincipalProvider
16 | {
17 | private readonly IHttpContextAccessor _httpContextAccessor;
18 |
19 | ///
20 | /// Initializes a new instance of the class.
21 | ///
22 | /// The HTTP context accessor
23 | public ClaimsPrincipalProvider(IHttpContextAccessor httpContextAccessor)
24 | {
25 | _httpContextAccessor = httpContextAccessor;
26 | }
27 |
28 | ///
29 | public ClaimsPrincipal GetUser()
30 | {
31 | return _httpContextAccessor.HttpContext.User;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.27/02-alter-function.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.getappevents(
2 | _subject character varying,
3 | _after character varying,
4 | _from timestamp with time zone,
5 | _to timestamp with time zone,
6 | _type text[],
7 | _source text[],
8 | _size integer)
9 | RETURNS TABLE(cloudevents text)
10 | LANGUAGE 'plpgsql'
11 | COST 100
12 | VOLATILE PARALLEL UNSAFE
13 | ROWS 1000
14 |
15 | AS $BODY$
16 |
17 | BEGIN
18 | return query
19 | SELECT cast(cloudevent as text) as cloudevents
20 | FROM events.events
21 | WHERE (_subject = '' OR cloudevent @> (select '{"subject": "' || _subject || '"}')::jsonb )
22 | AND (_from IS NULL OR (cloudevent->>'time')::timestamptz >= _from)
23 | AND (_to IS NULL OR (cloudevent->>'time')::timestamptz <= _to)
24 | AND (_type IS NULL OR cloudevent->>'type' ILIKE ANY(_type))
25 | AND (_source IS NULL OR cloudevent->>'source' ILIKE ANY(_source))
26 | AND (_after = '' OR sequenceno >(
27 | SELECT
28 | case count(*)
29 | when 0
30 | then 0
31 | else
32 | (SELECT sequenceno
33 | FROM events.events
34 | WHERE cloudevent->>'id' = _after
35 | ORDER BY sequenceno ASC
36 | LIMIT 1)
37 | end
38 | FROM events.events
39 | WHERE cloudevent->>'id' = _after))
40 | ORDER BY sequenceno
41 | limit _size;
42 | END;
43 | $BODY$;
--------------------------------------------------------------------------------
/src/Events/Services/Interfaces/IRegisterService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 |
5 | namespace Altinn.Platform.Events.Services.Interfaces;
6 |
7 | ///
8 | /// Represents a type that can be used to handle communication with the Register application API.
9 | ///
10 | public interface IRegisterService
11 | {
12 | ///
13 | /// Party lookup
14 | ///
15 | /// organisation number
16 | /// f or d number
17 | ///
18 | Task PartyLookup(string orgNo, string person);
19 |
20 | ///
21 | /// Perform a register lookup with urn based party identifiers in order to find all other party identifiers.
22 | ///
23 | /// List of urn values with a party identifying value
24 | ///
25 | /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
26 | ///
27 | /// A list of the perties found in register
28 | Task> PartyLookup(
29 | IEnumerable partyUrnList,
30 | CancellationToken cancellationToken);
31 | }
32 |
--------------------------------------------------------------------------------
/src/Events/Configuration/QueueStorageSettings.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 |
3 | namespace Altinn.Platform.Events.Configuration
4 | {
5 | ///
6 | /// Configuration object used to hold settings for the queue storage.
7 | ///
8 | public class QueueStorageSettings
9 | {
10 | ///
11 | /// ConnectionString for the storage account
12 | ///
13 | public string ConnectionString { get; set; }
14 |
15 | ///
16 | /// Name of the queue to push incoming events to, before persisting to db.
17 | ///
18 | public string RegistrationQueueName { get; set; }
19 |
20 | ///
21 | /// Name of the queue to push incoming events to, after persisting to db.
22 | /// Serviced by EventsInbound Azure Function
23 | ///
24 | public string InboundQueueName { get; set; }
25 |
26 | ///
27 | /// Name of queue serviced by EventsOutbound Azure Function
28 | ///
29 | public string OutboundQueueName { get; set; }
30 |
31 | ///
32 | /// Queue serviced by SubscriptionValidation Azure Function
33 | ///
34 | public string ValidationQueueName { get; set; }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Events/Services/Interfaces/ISubscriptionService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading.Tasks;
3 |
4 | using Altinn.Platform.Events.Models;
5 |
6 | namespace Altinn.Platform.Events.Services.Interfaces
7 | {
8 | ///
9 | /// Interface for subscription service
10 | ///
11 | public interface ISubscriptionService
12 | {
13 | ///
14 | /// Operation to delete a given subscriptions
15 | ///
16 | public Task DeleteSubscription(int id);
17 |
18 | ///
19 | /// Update validation status
20 | ///
21 | public Task<(Subscription Subscription, ServiceError Error)> SetValidSubscription(int id);
22 |
23 | ///
24 | /// Get a given subscription
25 | ///
26 | /// The subscription Id
27 | public Task<(Subscription Subscription, ServiceError Error)> GetSubscription(int id);
28 |
29 | ///
30 | /// Retrieves all subscriptions for the given consumer
31 | ///
32 | /// A list of subscriptions created by the current user.
33 | public Task<(List Subscription, ServiceError Error)> GetAllSubscriptions();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.37/01-alter-function.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.getevents(
2 | _resource character varying,
3 | _subject character varying,
4 | _alternativesubject character varying,
5 | _after character varying,
6 | _type text[],
7 | _size integer)
8 | RETURNS TABLE(cloudevents text)
9 | LANGUAGE 'plpgsql'
10 | COST 100
11 | STABLE PARALLEL SAFE
12 | ROWS 1000
13 |
14 | AS $BODY$
15 |
16 | DECLARE
17 | _sequenceno bigint;
18 | BEGIN
19 | IF _after IS NOT NULL AND _after <> '' THEN
20 | SELECT
21 | case count(*)
22 | when 0
23 | then 0
24 | else
25 | (SELECT MIN(sequenceno) FROM events.events
26 | WHERE cloudevent->>'id' = _after)
27 | end
28 | INTO _sequenceno
29 | FROM events.events
30 | WHERE cloudevent->>'id' = _after;
31 | END IF;
32 | return query
33 | SELECT cast(cloudevent as text) as cloudevents
34 | FROM events.events
35 | WHERE cloudevent->>'resource' = _resource
36 | AND (_subject IS NULL OR cloudevent->>'subject' = _subject)
37 | AND (_alternativeSubject IS NULL OR cloudevent->>'alternativesubject' = _alternativesubject)
38 | AND (_type IS NULL OR cloudevent->>'type' LIKE ANY(_type) )
39 | AND registeredtime <= now() - interval '30 second'
40 | AND (_after IS NULL OR _after = '' OR sequenceno > _sequenceno)
41 | ORDER BY sequenceno
42 | limit _size;
43 | END;
44 | $BODY$;
--------------------------------------------------------------------------------
/src/Events/Migration/FunctionsAndProcedures/findsubscription.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.find_subscription(
2 | _resourcefilter character varying,
3 | _sourcefilter character varying,
4 | _subjectfilter character varying,
5 | _typefilter character varying,
6 | _consumer character varying,
7 | _endpointurl character varying)
8 | RETURNS TABLE(id bigint, resourcefilter character varying, sourcefilter character varying, subjectfilter character varying, typefilter character varying, consumer character varying, endpointurl character varying, createdby character varying, validated boolean, "time" timestamp with time zone)
9 | LANGUAGE 'plpgsql'
10 | AS $BODY$
11 |
12 | BEGIN
13 | RETURN query
14 | SELECT
15 | s.id, s.resourcefilter, s.sourcefilter, s.subjectfilter, s.typefilter, s.consumer, s.endpointurl, s.createdby, s.validated, s."time"
16 | FROM
17 | events.subscription s
18 | WHERE
19 | s.resourcefilter = _resourcefilter
20 | AND ((_sourcefilter IS NULL AND s.sourcefilter IS NULL) OR s.sourcefilter = _sourcefilter)
21 | AND ((_subjectfilter IS NULL AND s.subjectfilter IS NULL) OR s.subjectfilter = _subjectfilter)
22 | AND ((_typefilter IS NULL AND s.typefilter IS NULL) OR s.typefilter = _typefilter)
23 | AND s.consumer = _consumer
24 | AND s.endpointurl = _endpointurl;
25 | END
26 | $BODY$;
--------------------------------------------------------------------------------
/src/Events/Migration/v0.35/01-alter-function.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.getevents(
2 | _resource character varying,
3 | _subject character varying,
4 | _alternativesubject character varying,
5 | _after character varying,
6 | _type text[],
7 | _size integer)
8 | RETURNS TABLE(cloudevents text)
9 | LANGUAGE 'plpgsql'
10 | COST 100
11 | STABLE PARALLEL SAFE
12 | ROWS 1000
13 |
14 | AS $BODY$
15 |
16 | DECLARE
17 | _sequenceno bigint;
18 | BEGIN
19 | IF _after IS NOT NULL AND _after <> '' THEN
20 | SELECT
21 | case count(*)
22 | when 0
23 | then 0
24 | else
25 | (SELECT sequenceno FROM events.events
26 | WHERE cloudevent->>'id' = _after
27 | ORDER BY sequenceno ASC)
28 | end
29 | INTO _sequenceno
30 | FROM events.events
31 | WHERE cloudevent->>'id' = _after;
32 | END IF;
33 | return query
34 | SELECT cast(cloudevent as text) as cloudevents
35 | FROM events.events
36 | WHERE cloudevent->>'resource' = _resource
37 | AND (_subject IS NULL OR cloudevent->>'subject' = _subject)
38 | AND (_alternativeSubject IS NULL OR cloudevent->>'alternativesubject' = _alternativesubject)
39 | AND (_type IS NULL OR cloudevent->>'type' LIKE ANY(_type) )
40 | AND registeredtime <= now() - interval '30 second'
41 | AND (_after IS NULL OR _after = '' OR sequenceno > _sequenceno)
42 | ORDER BY sequenceno
43 | limit _size;
44 | END;
45 | $BODY$;
--------------------------------------------------------------------------------
/src/Events/Models/TraceLogActivity.cs:
--------------------------------------------------------------------------------
1 | namespace Altinn.Platform.Events.Models
2 | {
3 | ///
4 | /// Enum for trace log activity
5 | ///
6 | public enum TraceLogActivity
7 | {
8 | ///
9 | /// Event is registered
10 | ///
11 | Registered,
12 |
13 | ///
14 | /// Response from post to consumer endpoint
15 | ///
16 | WebhookPostResponse,
17 |
18 | ///
19 | /// Trace log when is sent to outbound queue
20 | ///
21 | OutboundQueue,
22 |
23 | ///
24 | /// Subscriber was unauthorized for the event
25 | ///
26 | Unauthorized,
27 |
28 | ///
29 | /// Number of retries for the event has been reached
30 | ///
31 | DequeueLimitReached,
32 |
33 | ///
34 | /// Subscription is invalid
35 | ///
36 | InvalidSubscription,
37 |
38 | ///
39 | /// The response code is 2xx ie successful
40 | ///
41 | EndpointValidationSuccess,
42 |
43 | ///
44 | /// The response code implies that validation failed
45 | ///
46 | EndpointValidationFailed,
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/.github/workflows/container-scan.yml:
--------------------------------------------------------------------------------
1 | name: Events Scan
2 |
3 | on:
4 | schedule:
5 | - cron: '0 8 * * 1,4'
6 | push:
7 | branches: [ main ]
8 | paths:
9 | - 'src/Events/**'
10 | - 'Dockerfile'
11 | pull_request:
12 | branches: [ main ]
13 | types: [opened, synchronize, reopened]
14 | paths:
15 | - 'src/Events/**'
16 | - 'Dockerfile'
17 | jobs:
18 | scan:
19 | runs-on: ubuntu-latest
20 | permissions:
21 | contents: read
22 | steps:
23 | - name: Checkout code
24 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
25 |
26 | - name: Build the Docker image
27 | run: docker build . --tag altinn-events:${{github.sha}}
28 |
29 | - name: Run Trivy vulnerability scanner
30 | uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
31 | with:
32 | image-ref: 'altinn-events:${{ github.sha }}'
33 | format: 'table'
34 | exit-code: '1'
35 | ignore-unfixed: true
36 | vuln-type: 'os,library'
37 | severity: 'CRITICAL,HIGH'
38 | env:
39 | TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db,aquasec/trivy-db,ghcr.io/aquasecurity/trivy-db
40 | TRIVY_JAVA_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-java-db,aquasec/trivy-java-db,ghcr.io/aquasecurity/trivy-java-db
41 |
--------------------------------------------------------------------------------
/src/Events/Extensions/NpgsqlParameterCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | using System;
4 |
5 | using Npgsql;
6 |
7 | namespace Altinn.Platform.Events.Extensions;
8 |
9 | ///
10 | /// This class contains a set of extension methods for the class.
11 | ///
12 | public static class NpgsqlParameterCollectionExtensions
13 | {
14 | ///
15 | /// Add a new parameter to the collection. Provide it with the given value if defined.
16 | /// If value is null give the parameter DBNull.Value instead.
17 | ///
18 | /// The to add a new parameter to.
19 | /// The name of the new parameter to be added.
20 | /// The nullable string value to be given to the parameter.
21 | /// The created with given name and appropriate value.
22 | public static NpgsqlParameter AddWithNullableString(
23 | this NpgsqlParameterCollection collection, string parameterName, string? value)
24 | {
25 | if (value is not null)
26 | {
27 | return collection.AddWithValue(parameterName, value);
28 | }
29 |
30 | return collection.AddWithValue(parameterName, DBNull.Value);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Events.Functions/Extensions/CloudEventExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Text;
3 |
4 | using CloudNative.CloudEvents;
5 | using CloudNative.CloudEvents.SystemTextJson;
6 |
7 | namespace Altinn.Platform.Events.Functions.Extensions
8 | {
9 | ///
10 | /// Extension methods for cloud events
11 | ///
12 | public static class CloudEventExtensions
13 | {
14 | ///
15 | /// Serializes the cloud event using a JsonEventFormatter
16 | ///
17 | /// The json serialized cloud event
18 | public static string Serialize(this CloudEvent cloudEvent)
19 | {
20 | var formatter = new JsonEventFormatter();
21 | var bytes = formatter.EncodeStructuredModeMessage(cloudEvent, out _);
22 | return Encoding.UTF8.GetString(bytes.Span);
23 | }
24 |
25 | ///
26 | /// Deserializes a json string to a the cloud event using a JsonEventFormatter
27 | ///
28 | /// The cloud event
29 | public static CloudEvent DeserializeToCloudEvent(this string item)
30 | {
31 | var formatter = new JsonEventFormatter();
32 |
33 | var cloudEvent = formatter.DecodeStructuredModeMessage(new MemoryStream(Encoding.UTF8.GetBytes(item)), null, null);
34 | return cloudEvent;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/test/k6/src/api/subscriptions.js:
--------------------------------------------------------------------------------
1 | import http from "k6/http";
2 | import { stopIterationOnFail } from "../errorhandler.js";
3 | import * as apiHelpers from "../apiHelpers.js";
4 | import * as config from "../config.js";
5 |
6 | export function getAllSubscriptions(token) {
7 | var endpoint = config.platformEvents.subscriptions;
8 | return getSubscriptions(endpoint, token);
9 | }
10 |
11 | export function getSubscriptionById(id, token) {
12 | var endpoint = config.platformEvents.subscriptions + "/" + id;
13 | return getSubscriptions(endpoint, token);
14 | }
15 |
16 | export function postSubscription(serializedSubscription, token) {
17 | var endpoint = config.platformEvents.subscriptions;
18 |
19 | var params = apiHelpers.buildHeaderWithBearerAndContentType(
20 | token,
21 | "application/json"
22 | );
23 |
24 | var response = http.post(endpoint, serializedSubscription, params);
25 |
26 | return response;
27 | }
28 |
29 | export function deleteSubscription(id, token) {
30 | var endpoint = config.platformEvents.subscriptions + id;
31 | var params = apiHelpers.buildHeaderWithBearer(token);
32 | var response = http.del(endpoint,null, params);
33 | return response;
34 | }
35 |
36 | function getSubscriptions(endpoint, token) {
37 | var params = apiHelpers.buildHeaderWithBearerAndContentType(
38 | token,
39 | "application/json"
40 | );
41 |
42 | var response = http.get(endpoint, params);
43 |
44 | return response;
45 | }
46 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/TestingExtensions/UriExtensionsTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | using Altinn.Platform.Events.Extensions;
4 |
5 | using Xunit;
6 |
7 | namespace Altinn.Platform.Events.Tests.TestingExtensions
8 | {
9 | public class UriExtensionsTests
10 | {
11 | [Theory]
12 | [InlineData("https://ttd.apps.altinn.cloud/ttd/apps-test", true)]
13 | [InlineData("https://ttd.apps.altinn.cloud/ttd/apps-test-v2/", true)]
14 | [InlineData("urn:namespaceid:ttd:apps:apps-test", true)]
15 | [InlineData("urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66", true)]
16 | [InlineData("telnet://ole:qwerty@altinn.no:45432/", false)]
17 | [InlineData("http://vg.no", false)]
18 | public void IsValidUrlOrUrn(string uri, bool expected)
19 | {
20 | bool actual = UriExtensions.IsValidUrlOrUrn(new Uri(uri));
21 |
22 | Assert.Equal(expected, actual);
23 | }
24 |
25 | [Theory]
26 | [InlineData("urn:namespaceid:ttd:apps:apps-test", true)]
27 | [InlineData("urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66", true)]
28 | [InlineData(" urn:namespaceid:ttd:apps:apps-test", false)]
29 | [InlineData("urn:foo::::", false)]
30 | public void IsValidUrn(string urn, bool expected)
31 | {
32 | bool actual = UriExtensions.IsValidUrn(urn);
33 |
34 | Assert.Equal(expected, actual);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.00/01-setup-tables.sql:
--------------------------------------------------------------------------------
1 | -- SCHEMA: events
2 |
3 | CREATE SCHEMA IF NOT EXISTS events
4 | AUTHORIZATION platform_events_admin;
5 |
6 | -- Table: events.events
7 |
8 | CREATE TABLE IF NOT EXISTS events.events
9 | (
10 | sequenceno BIGSERIAL,
11 | id character varying COLLATE pg_catalog."default" NOT NULL,
12 | source character varying COLLATE pg_catalog."default" NOT NULL,
13 | subject character varying COLLATE pg_catalog."default" NOT NULL,
14 | "time" timestamptz NOT NULL,
15 | type character varying COLLATE pg_catalog."default" NOT NULL,
16 | cloudevent text COLLATE pg_catalog."default" NOT NULL,
17 | CONSTRAINT events_pkey PRIMARY KEY (sequenceno)
18 | )
19 |
20 | TABLESPACE pg_default;
21 |
22 | -- Procecure: insert_event
23 |
24 | CREATE OR REPLACE PROCEDURE events.insert_event(
25 | id character varying,
26 | source character varying,
27 | subject character varying,
28 | type character varying,
29 | cloudevent text)
30 | LANGUAGE 'plpgsql'
31 | AS $BODY$
32 | DECLARE currentTime timestamptz;
33 | DECLARE currentTimeString character varying;
34 | BEGIN
35 | SET TIME ZONE UTC;
36 | currentTime := NOW();
37 | currentTimeString := to_char(currentTime, 'YYYY-MM-DD"T"HH24:MI:SS.USOF');
38 |
39 | INSERT INTO events.events(id, source, subject, type, "time", cloudevent)
40 | VALUES ($1, $2, $3, $4, currentTime, substring($5 from 1 for length($5) -1) || ',"time": "' || currentTimeString || '"}');
41 |
42 | END;
43 | $BODY$;
44 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.38/01-create-function.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.getappevents_v2(
2 | _subject character varying,
3 | _after character varying,
4 | _from timestamp with time zone,
5 | _to timestamp with time zone,
6 | _type text[],
7 | _source text[],
8 | _resource text,
9 | _size integer)
10 | RETURNS TABLE(cloudevents text)
11 | LANGUAGE 'plpgsql'
12 | ROWS 1000
13 |
14 | AS $BODY$
15 | DECLARE
16 | _sequenceno bigint;
17 | BEGIN
18 | IF _after IS NOT NULL AND _after <> '' THEN
19 | SELECT
20 | case count(*)
21 | when 0
22 | then 0
23 | else
24 | (SELECT MIN(sequenceno)
25 | FROM events.events
26 | WHERE cloudevent->>'id' = _after)
27 | end
28 | INTO _sequenceno
29 | FROM events.events
30 | WHERE cloudevent->>'id' = _after;
31 | END IF;
32 | return query
33 | SELECT cast(cloudevent as text) as cloudevents
34 | FROM events.events
35 | WHERE (_subject IS NULL OR cloudevent->>'subject' = _subject)
36 | AND (_from IS NULL OR cloudevent->>'time' >= _from::text)
37 | AND (_to IS NULL OR cloudevent->>'time' <= _to::text)
38 | AND registeredtime <= now() - interval '30 second'
39 | AND (_type IS NULL OR cloudevent->>'type' ILIKE ANY(_type))
40 | AND (_source IS NULL OR cloudevent->>'source' ILIKE ANY(_source))
41 | AND (_resource IS NULL OR cloudevent->>'resource' = _resource)
42 | AND (_after IS NULL OR _after = '' OR sequenceno > _sequenceno)
43 | ORDER BY sequenceno
44 | limit _size;
45 | END;
46 | $BODY$;
47 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Roles/User_1337/party_500000/roles.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "Type": "altinn",
4 | "value": "DAGL"
5 | },
6 | {
7 | "Type": "altinn",
8 | "value": "LOPER"
9 | },
10 | {
11 | "Type": "altinn",
12 | "value": "ADMAI"
13 | },
14 | {
15 | "Type": "altinn",
16 | "value": "REGNA"
17 | },
18 | {
19 | "Type": "altinn",
20 | "value": "SISKD"
21 | },
22 | {
23 | "Type": "altinn",
24 | "value": "UILUF"
25 | },
26 | {
27 | "Type": "altinn",
28 | "value": "UTINN"
29 | },
30 | {
31 | "Type": "altinn",
32 | "value": "UTOMR"
33 | },
34 | {
35 | "Type": "altinn",
36 | "value": "KLADM"
37 | },
38 | {
39 | "Type": "altinn",
40 | "value": "ATTST"
41 | },
42 | {
43 | "Type": "altinn",
44 | "value": "HVASK"
45 | },
46 | {
47 | "Type": "altinn",
48 | "value": "PAVAD"
49 | },
50 | {
51 | "Type": "altinn",
52 | "value": "SIGNE"
53 | },
54 | {
55 | "Type": "altinn",
56 | "value": "KOMAB"
57 | },
58 | {
59 | "Type": "altinn",
60 | "value": "A0212"
61 | },
62 | {
63 | "Type": "altinn",
64 | "value": "ECKEYROLE"
65 | },
66 | {
67 | "Type": "altinn",
68 | "value": "PASIG"
69 | },
70 | {
71 | "Type": "altinn",
72 | "value": "A0236"
73 | },
74 | {
75 | "Type": "altinn",
76 | "value": "A0278"
77 | },
78 | {
79 | "Type": "altinn",
80 | "value": "HADM"
81 | }
82 | ]
83 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Roles/User_1337/party_500001/roles.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "Type": "altinn",
4 | "value": "DAGL"
5 | },
6 | {
7 | "Type": "altinn",
8 | "value": "LOPER"
9 | },
10 | {
11 | "Type": "altinn",
12 | "value": "ADMAI"
13 | },
14 | {
15 | "Type": "altinn",
16 | "value": "REGNA"
17 | },
18 | {
19 | "Type": "altinn",
20 | "value": "SISKD"
21 | },
22 | {
23 | "Type": "altinn",
24 | "value": "UILUF"
25 | },
26 | {
27 | "Type": "altinn",
28 | "value": "UTINN"
29 | },
30 | {
31 | "Type": "altinn",
32 | "value": "UTOMR"
33 | },
34 | {
35 | "Type": "altinn",
36 | "value": "KLADM"
37 | },
38 | {
39 | "Type": "altinn",
40 | "value": "ATTST"
41 | },
42 | {
43 | "Type": "altinn",
44 | "value": "HVASK"
45 | },
46 | {
47 | "Type": "altinn",
48 | "value": "PAVAD"
49 | },
50 | {
51 | "Type": "altinn",
52 | "value": "SIGNE"
53 | },
54 | {
55 | "Type": "altinn",
56 | "value": "KOMAB"
57 | },
58 | {
59 | "Type": "altinn",
60 | "value": "A0212"
61 | },
62 | {
63 | "Type": "altinn",
64 | "value": "ECKEYROLE"
65 | },
66 | {
67 | "Type": "altinn",
68 | "value": "PASIG"
69 | },
70 | {
71 | "Type": "altinn",
72 | "value": "A0236"
73 | },
74 | {
75 | "Type": "altinn",
76 | "value": "A0278"
77 | },
78 | {
79 | "Type": "altinn",
80 | "value": "HADM"
81 | }
82 | ]
83 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Roles/User_1337/party_500002/roles.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "Type": "altinn",
4 | "value": "DAGL"
5 | },
6 | {
7 | "Type": "altinn",
8 | "value": "LOPER"
9 | },
10 | {
11 | "Type": "altinn",
12 | "value": "ADMAI"
13 | },
14 | {
15 | "Type": "altinn",
16 | "value": "REGNA"
17 | },
18 | {
19 | "Type": "altinn",
20 | "value": "SISKD"
21 | },
22 | {
23 | "Type": "altinn",
24 | "value": "UILUF"
25 | },
26 | {
27 | "Type": "altinn",
28 | "value": "UTINN"
29 | },
30 | {
31 | "Type": "altinn",
32 | "value": "UTOMR"
33 | },
34 | {
35 | "Type": "altinn",
36 | "value": "KLADM"
37 | },
38 | {
39 | "Type": "altinn",
40 | "value": "ATTST"
41 | },
42 | {
43 | "Type": "altinn",
44 | "value": "HVASK"
45 | },
46 | {
47 | "Type": "altinn",
48 | "value": "PAVAD"
49 | },
50 | {
51 | "Type": "altinn",
52 | "value": "SIGNE"
53 | },
54 | {
55 | "Type": "altinn",
56 | "value": "KOMAB"
57 | },
58 | {
59 | "Type": "altinn",
60 | "value": "A0212"
61 | },
62 | {
63 | "Type": "altinn",
64 | "value": "ECKEYROLE"
65 | },
66 | {
67 | "Type": "altinn",
68 | "value": "PASIG"
69 | },
70 | {
71 | "Type": "altinn",
72 | "value": "A0236"
73 | },
74 | {
75 | "Type": "altinn",
76 | "value": "A0278"
77 | },
78 | {
79 | "Type": "altinn",
80 | "value": "HADM"
81 | }
82 | ]
83 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Roles/User_1337/party_500003/roles.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "Type": "altinn",
4 | "value": "DAGL"
5 | },
6 | {
7 | "Type": "altinn",
8 | "value": "LOPER"
9 | },
10 | {
11 | "Type": "altinn",
12 | "value": "ADMAI"
13 | },
14 | {
15 | "Type": "altinn",
16 | "value": "REGNA"
17 | },
18 | {
19 | "Type": "altinn",
20 | "value": "SISKD"
21 | },
22 | {
23 | "Type": "altinn",
24 | "value": "UILUF"
25 | },
26 | {
27 | "Type": "altinn",
28 | "value": "UTINN"
29 | },
30 | {
31 | "Type": "altinn",
32 | "value": "UTOMR"
33 | },
34 | {
35 | "Type": "altinn",
36 | "value": "KLADM"
37 | },
38 | {
39 | "Type": "altinn",
40 | "value": "ATTST"
41 | },
42 | {
43 | "Type": "altinn",
44 | "value": "HVASK"
45 | },
46 | {
47 | "Type": "altinn",
48 | "value": "PAVAD"
49 | },
50 | {
51 | "Type": "altinn",
52 | "value": "SIGNE"
53 | },
54 | {
55 | "Type": "altinn",
56 | "value": "KOMAB"
57 | },
58 | {
59 | "Type": "altinn",
60 | "value": "A0212"
61 | },
62 | {
63 | "Type": "altinn",
64 | "value": "ECKEYROLE"
65 | },
66 | {
67 | "Type": "altinn",
68 | "value": "PASIG"
69 | },
70 | {
71 | "Type": "altinn",
72 | "value": "A0236"
73 | },
74 | {
75 | "Type": "altinn",
76 | "value": "A0278"
77 | },
78 | {
79 | "Type": "altinn",
80 | "value": "HADM"
81 | }
82 | ]
83 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/Roles/User_1337/party_500600/roles.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "Type": "altinn",
4 | "value": "LEDE"
5 | },
6 | {
7 | "Type": "altinn",
8 | "value": "LOPER"
9 | },
10 | {
11 | "Type": "altinn",
12 | "value": "ADMAI"
13 | },
14 | {
15 | "Type": "altinn",
16 | "value": "REGNA"
17 | },
18 | {
19 | "Type": "altinn",
20 | "value": "SISKD"
21 | },
22 | {
23 | "Type": "altinn",
24 | "value": "UILUF"
25 | },
26 | {
27 | "Type": "altinn",
28 | "value": "UTINN"
29 | },
30 | {
31 | "Type": "altinn",
32 | "value": "UTOMR"
33 | },
34 | {
35 | "Type": "altinn",
36 | "value": "KLADM"
37 | },
38 | {
39 | "Type": "altinn",
40 | "value": "ATTST"
41 | },
42 | {
43 | "Type": "altinn",
44 | "value": "HVASK"
45 | },
46 | {
47 | "Type": "altinn",
48 | "value": "PAVAD"
49 | },
50 | {
51 | "Type": "altinn",
52 | "value": "SIGNE"
53 | },
54 | {
55 | "Type": "altinn",
56 | "value": "KOMAB"
57 | },
58 | {
59 | "Type": "altinn",
60 | "value": "A0212"
61 | },
62 | {
63 | "Type": "altinn",
64 | "value": "ECKEYROLE"
65 | },
66 | {
67 | "Type": "altinn",
68 | "value": "PASIG"
69 | },
70 | {
71 | "Type": "altinn",
72 | "value": "A0236"
73 | },
74 | {
75 | "Type": "altinn",
76 | "value": "A0278"
77 | },
78 | {
79 | "Type": "altinn",
80 | "value": "HADM"
81 | }
82 | ]
83 |
--------------------------------------------------------------------------------
/src/Events/Services/PartiesRegisterQueryResponse.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | using System;
4 | using System.Collections.Generic;
5 |
6 | namespace Altinn.Platform.Events.Services;
7 |
8 | ///
9 | /// Data type used for querying parties in register based on URN strings.
10 | ///
11 | public record PartiesRegisterQueryResponse
12 | {
13 | ///
14 | /// List of valid URN strings with party identifying values.
15 | ///
16 | public required List Data { get; set; }
17 | }
18 |
19 | ///
20 | /// Represents a party with all possible identifiers.
21 | ///
22 | public record PartyIdentifiers
23 | {
24 | ///
25 | /// The type of party, either "person" or "organization".
26 | ///
27 | public required string PartyType { get; set; }
28 |
29 | ///
30 | /// The party's UUID value.
31 | ///
32 | public Guid PartyUuid { get; set; }
33 |
34 | ///
35 | /// The party's original party id (From Altinn 2).
36 | ///
37 | public int PartyId { get; set; }
38 |
39 | ///
40 | /// The party's national identity number if of type "person".
41 | ///
42 | public string? PersonIdentifier { get; set; }
43 |
44 | ///
45 | /// The party's organization number if of type "organization".
46 | ///
47 | public string? OrganizationIdentifier { get; set; }
48 | }
49 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/TestingExtensions/NpgsqlParameterCollectionExtensionsTests.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | using System;
4 |
5 | using Altinn.Platform.Events.Extensions;
6 |
7 | using Npgsql;
8 |
9 | using Xunit;
10 |
11 | namespace Altinn.Platform.Events.Tests.Extensions;
12 |
13 | public class NpgsqlParameterCollectionExtensionsTests
14 | {
15 | [Fact]
16 | public void AddWithNullableString_ValueIsNotNull_ParameterValueMatchInput()
17 | {
18 | // Arrange
19 | NpgsqlCommand command = new(); // Can't construct a parameter collection directly.
20 |
21 | const string ParameterName = "parameterName";
22 | const string ParameterValue = "test";
23 |
24 | // Act
25 | command.Parameters.AddWithNullableString(ParameterName, ParameterValue);
26 |
27 | // Assert
28 | Assert.Equal(ParameterValue, command.Parameters[ParameterName].Value);
29 | }
30 |
31 | [Fact]
32 | public void AddWithNullableString_ValueIsNull_ParameterValueIsDBNull()
33 | {
34 | // Arrange
35 | NpgsqlCommand command = new(); // Can't construct a parameter collection directly.
36 |
37 | const string ParameterName = "parameterName";
38 | const string? ParameterValue = null;
39 |
40 | // Act
41 | command.Parameters.AddWithNullableString(ParameterName, ParameterValue);
42 |
43 | // Assert
44 | Assert.Equal(DBNull.Value, command.Parameters[ParameterName].Value);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.03/01-setup-tables.sql:
--------------------------------------------------------------------------------
1 | -- Table: events.subscription
2 |
3 | CREATE TABLE IF NOT EXISTS events.subscription
4 | (
5 | id BIGSERIAL,
6 | sourcefilter character varying COLLATE pg_catalog."default",
7 | subjectfilter character varying COLLATE pg_catalog."default",
8 | typefilter character varying COLLATE pg_catalog."default",
9 | consumer character varying COLLATE pg_catalog."default" NOT NULL,
10 | endpointurl character varying COLLATE pg_catalog."default" NOT NULL,
11 | createdby character varying COLLATE pg_catalog."default" NOT NULL,
12 | validated BOOLEAN NOT NULL,
13 | "time" timestamptz NOT NULL,
14 | CONSTRAINT eventssubscription_pkey PRIMARY KEY (id)
15 | )
16 |
17 |
18 | TABLESPACE pg_default;
19 |
20 | CREATE OR REPLACE PROCEDURE events.insert_subcsription(
21 | sourcefilter character varying,
22 | subjectfilter character varying,
23 | typefilter character varying,
24 | consumer character varying,
25 | endpointurl character varying,
26 | createdby character varying,
27 | validated boolean,
28 | inout subscription_id bigint)
29 | LANGUAGE 'plpgsql'
30 | AS $BODY$
31 | DECLARE currentTime timestamptz;
32 |
33 | BEGIN
34 | SET TIME ZONE UTC;
35 | currentTime := NOW();
36 |
37 | INSERT INTO events.subscription(sourcefilter, subjectfilter, typefilter, consumer, endpointurl, createdby, "time", validated)
38 | VALUES ($1, $2, $3, $4, $5, $6, currentTime, $7) RETURNING (id) into subscription_id;
39 | END;
40 | $BODY$;
41 |
42 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.04/02-setup-functions.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.getsubscriptionsexcludeorgs(source character varying, subject character varying, type character varying)
2 | RETURNS TABLE (id bigint, sourcefilter character varying, subjectfilter character varying, typefilter character varying, consumer character varying, endpointurl character varying, createdby character varying, validated BOOLEAN, "time" timestamptz)
3 | LANGUAGE 'plpgsql'
4 |
5 | AS $BODY$
6 | BEGIN
7 | return query
8 | SELECT s.id, s.sourcefilter, s.subjectfilter, s.typefilter, s.consumer, s.endpointurl, s.createdby, s.validated, s."time"
9 | FROM events.subscription s
10 | WHERE (s.subjectfilter = subject)
11 | AND (s.sourcefilter = source)
12 | AND (s.typefilter is NULL OR s.typefilter = type)
13 | AND s.consumer not LIKE '/org/%';
14 |
15 | END;
16 | $BODY$;
17 |
18 | CREATE OR REPLACE FUNCTION events.getsubscriptionsbyconsumer(_consumer character varying)
19 | RETURNS TABLE (id bigint, sourcefilter character varying, subjectfilter character varying, typefilter character varying, consumer character varying, endpointurl character varying, createdby character varying, validated BOOLEAN, "time" timestamptz)
20 | LANGUAGE 'plpgsql'
21 |
22 | AS $BODY$
23 | BEGIN
24 | return query
25 | SELECT s.id, s.sourcefilter, s.subjectfilter, s.typefilter, s.consumer, s.endpointurl, s.createdby, s.validated, s."time"
26 | FROM events.subscription s
27 | WHERE s.consumer LIKE _consumer;
28 |
29 | END;
30 | $BODY$;
31 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/events/3.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "sequenceno": 1,
4 | "id": "e31dbb11-2208-4dda-a549-92a0db8c7708",
5 | "source": "urn:altinn:systemx",
6 | "resource": "urn:altinn:resource:systemx",
7 | "subject": "/party/1337",
8 | "time": "2020-10-13T11:50:29Z",
9 | "type": "instance.deleted",
10 | "alternativesubject": "/person/01038712345",
11 | "cloudEvent": {
12 | "id": "e31dbb11-2208-4dda-a549-92a0db8c7708",
13 | "source": "urn:altinn:systemx",
14 | "resource": "urn:altinn:resource:systemx",
15 | "specversion": "1.0",
16 | "type": "instance.deleted",
17 | "subject": "/party/1337",
18 | "alternativesubject": "/person/01038712345",
19 | "time": "2020-10-13T11:50:29Z"
20 | }
21 | },
22 | {
23 | "sequenceno": 2,
24 | "id": "e31dbb11-2208-4dda-a549-92a0db8c8808",
25 | "source": "urn:altinn:systemx",
26 | "resource": "urn:altinn:resource:systemx",
27 | "subject": "/party/1337",
28 | "time": "2020-10-13T11:50:29Z",
29 | "type": "instance.restored",
30 | "alternativesubject": "/person/01038712345",
31 | "cloudEvent": {
32 | "id": "e31dbb11-2208-4dda-a549-92a0db8c8808",
33 | "resource": "urn:altinn:resource:systemx",
34 | "source": "urn:altinn:systemx",
35 | "specversion": "1.0",
36 | "type": "instance.deleted",
37 | "subject": "/party/1337",
38 | "alternativesubject": "/person/01038712345",
39 | "time": "2020-10-13T12:50:29Z"
40 | }
41 | }
42 | ]
--------------------------------------------------------------------------------
/src/Events/Migration/v0.33/01-alter-function.sql:
--------------------------------------------------------------------------------
1 | DROP FUNCTION IF EXISTS events.getappevents(character varying, character varying, timestamp with time zone, timestamp with time zone, text[], text[], integer);
2 | CREATE OR REPLACE FUNCTION events.getappevents(
3 | _subject character varying,
4 | _after character varying,
5 | _from timestamp with time zone,
6 | _to timestamp with time zone,
7 | _type text[],
8 | _source text[],
9 | _size integer)
10 | RETURNS TABLE(cloudevents text)
11 | LANGUAGE 'plpgsql'
12 |
13 | AS $BODY$
14 | DECLARE
15 | _sequenceno bigint;
16 | BEGIN
17 | IF _after IS NOT NULL AND _after <> '' THEN
18 | SELECT
19 | case count(*)
20 | when 0
21 | then 0
22 | else
23 | (SELECT sequenceno FROM events.events
24 | WHERE cloudevent->>'id' = _after
25 | ORDER BY sequenceno ASC)
26 | end
27 | INTO _sequenceno
28 | FROM events.events
29 | WHERE cloudevent->>'id' = _after;
30 | END IF;
31 | RETURN query
32 | SELECT cast(cloudevent as text) as cloudevents
33 | FROM events.events
34 | WHERE (_subject IS NULL OR _subject = '' OR cloudevent->>'subject' = _subject)
35 | AND (_from IS NULL OR cloudevent->>'time' >= _from::text)
36 | AND (_to IS NULL OR cloudevent->>'time' <= _to::text)
37 | AND registeredtime <= now() - interval '30 second'
38 | AND (_type IS NULL OR cloudevent->>'type' ILIKE ANY(_type))
39 | AND (_source IS NULL OR cloudevent->>'source' ILIKE ANY(_source))
40 | AND (_after IS NULL OR _after = '' OR sequenceno > _sequenceno)
41 | ORDER BY sequenceno
42 | limit _size;
43 | END;
44 | $BODY$;
45 |
--------------------------------------------------------------------------------
/src/Events/Extensions/HttpRequestExtension.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Text;
3 | using System.Threading.Tasks;
4 | using Microsoft.AspNetCore.Http;
5 |
6 | namespace Altinn.Platform.Events.Extensions
7 | {
8 | ///
9 | /// This extension is created to make it easy to add a bearer token to a HttpRequests.
10 | ///
11 | public static class HttpRequestExtension
12 | {
13 | ///
14 | /// Retrieves raw string using specified encoding, or UTF8 otherwise.
15 | ///
16 | /// Remember to add to service configuration.
17 | /// Request input
18 | /// Optional encoding
19 | ///
20 | public static async Task GetRawBodyAsync(this HttpRequest request, Encoding encoding = null)
21 | {
22 | if (!request.Body.CanSeek)
23 | {
24 | // We only do this if the stream isn't *already* seekable,
25 | // as EnableBuffering will create a new stream instance
26 | // each time it's called
27 | request.EnableBuffering();
28 | }
29 |
30 | request.Body.Position = 0;
31 |
32 | var reader = new StreamReader(request.Body, encoding ?? Encoding.UTF8);
33 |
34 | var body = await reader.ReadToEndAsync().ConfigureAwait(false);
35 |
36 | request.Body.Position = 0;
37 |
38 | return body;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/JWTValidationCert.cer:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIID/zCCAuegAwIBAgIQF2ov3ZZUmJVKtoz0a1fabDANBgkqhkiG9w0BAQsFADB/
3 | MRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHY29udG9zbzEU
4 | MBIGCgmSJomT8ixkARkWBGNvcnAxFTATBgNVBAsMDFVzZXJBY2NvdW50czEiMCAG
5 | A1UEAwwZQWx0aW5uIFBsYXRmb3JtIFVuaXQgdGVzdDAgFw0yMDA0MTQwOTMwMTda
6 | GA8yMTIwMDQxNDA5NDAxOFowfzETMBEGCgmSJomT8ixkARkWA2NvbTEXMBUGCgmS
7 | JomT8ixkARkWB2NvbnRvc28xFDASBgoJkiaJk/IsZAEZFgRjb3JwMRUwEwYDVQQL
8 | DAxVc2VyQWNjb3VudHMxIjAgBgNVBAMMGUFsdGlubiBQbGF0Zm9ybSBVbml0IHRl
9 | c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCAKc+q5jbYFyQFxM1
10 | xU3v0N477ppnMu03K8qlEkX0+yffRHcR1I0Kku8yg1S+LQjeqh1K42b270myKiIt
11 | vxeuNnanRwdehTZthThembr8RXoGcmzaXfMet7NVDgUa7gNzPXbqjhTFdyWoZzeU
12 | X6TWTgFtciTs5M1F50H+3nieGKX2dvLUIEXWFO7yevj9bqtI8k0b66eLgBjchnjW
13 | 8B7oYOFZW44VDDnqQrvFJ9aMQ44FfLAWWLcy6nBzcDdK+Z+yq9FNVgduyl0J7vRo
14 | 3UtcVazLUvmDdwASLIB3IwB7YmT6fuOyM+6eyw5F1CdjXbc/bhop0pCDY1aAEsZA
15 | CjT9AgMBAAGjdTBzMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcD
16 | AjAtBgNVHREEJjAkoCIGCisGAQQBgjcUAgOgFAwSdGVzdEBhbHRpbm4uc3R1ZGlv
17 | MB0GA1UdDgQWBBTv8Cpf5J7nfmGds20LU/J3bg05XTANBgkqhkiG9w0BAQsFAAOC
18 | AQEAahWeu6ymaiJe9+LiMlQwNsUIV4KaLX+jCsRyF1jUJ0C13aFALGM4k9svqqXR
19 | DzBdCXXr0c1E+Ks3sCwBLfK5yj5fTI+pL26ceEmHahcVyLvzEBljtNb4FnGFs92P
20 | CH0NuCz45hQ2O9/Tv4cZAdgledTznJTKzzQNaF8M6iINmP6sf4kOg0BQx0K71K4f
21 | 7j2oQvYKiT7Zv1e83cdk9pS4ihDe+ZWYiGUM/IuaXNPl6OzVk4rY88PZJAoz7q33
22 | rYjlT+zkcl3dzTc3E0CWzbIWjhaXCRWvlI44cLRtdpmPqJUHI6a/tcGwNb5vWiT4
23 | YfZJ0EZ2iSRQlpU3+jMs8Ci2AA==
24 | -----END CERTIFICATE-----
25 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.07/01-setup-functions.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.getsubscriptionsexcludeorgs(source character varying, subject character varying, type character varying)
2 | RETURNS TABLE (id bigint, sourcefilter character varying, subjectfilter character varying, typefilter character varying, consumer character varying, endpointurl character varying, createdby character varying, validated BOOLEAN, "time" timestamptz)
3 | LANGUAGE 'plpgsql'
4 |
5 | AS $BODY$
6 | BEGIN
7 | return query
8 | SELECT s.id, s.sourcefilter, s.subjectfilter, s.typefilter, s.consumer, s.endpointurl, s.createdby, s.validated, s."time"
9 | FROM events.subscription s
10 | WHERE (s.subjectfilter = subject)
11 | AND (s.sourcefilter = source)
12 | AND (s.typefilter is NULL OR s.typefilter = type)
13 | AND s.consumer not LIKE '/org/%'
14 | AND s.validated = true;
15 |
16 | END;
17 | $BODY$;
18 |
19 | CREATE OR REPLACE FUNCTION events.getsubscriptionsbyconsumer(_consumer character varying)
20 | RETURNS TABLE (id bigint, sourcefilter character varying, subjectfilter character varying, typefilter character varying, consumer character varying, endpointurl character varying, createdby character varying, validated BOOLEAN, "time" timestamptz)
21 | LANGUAGE 'plpgsql'
22 |
23 | AS $BODY$
24 | BEGIN
25 | return query
26 | SELECT s.id, s.sourcefilter, s.subjectfilter, s.typefilter, s.consumer, s.endpointurl, s.createdby, s.validated, s."time"
27 | FROM events.subscription s
28 | WHERE s.consumer LIKE _consumer
29 | AND s.validated = true;
30 |
31 | END;
32 | $BODY$;
33 |
--------------------------------------------------------------------------------
/src/Events.Common/Models/RetryableEventWrapper.cs:
--------------------------------------------------------------------------------
1 | namespace Altinn.Platform.Events.Common.Models;
2 |
3 | ///
4 | /// This record contains metadata related to manual retry operations.
5 | ///
6 | public record RetryableEventWrapper
7 | {
8 | private static readonly System.Text.Json.JsonSerializerOptions _serializerOptions = new System.Text.Json.JsonSerializerOptions
9 | {
10 | WriteIndented = false,
11 | PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase
12 | };
13 |
14 | ///
15 | /// Contains the actual payload of the event as a serialized string.
16 | ///
17 | public required string Payload { get; set; }
18 |
19 | ///
20 | /// Specifies the number of times the message has been retried.
21 | ///
22 | public int DequeueCount { get; set; } = 0;
23 |
24 | ///
25 | /// Unique identifier for all events belonging to the same content, across retries.
26 | ///
27 | public string CorrelationId { get; set; } = Guid.NewGuid().ToString();
28 |
29 | ///
30 | /// Timestamp indicating when the event was first fired.
31 | ///
32 | public DateTime FirstProcessedAt { get; set; } = DateTime.UtcNow;
33 |
34 | ///
35 | /// Serializes the current instance to a JSON string representation.
36 | ///
37 | ///
38 | public string Serialize()
39 | {
40 | return System.Text.Json.JsonSerializer.Serialize(this, _serializerOptions);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Events/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "PostgreSQLSettings": {
3 | "EnableDBConnection": "true",
4 | "WorkspacePath": "Migration",
5 | "AdminConnectionString": "Host=localhost;Port=5432;Username=platform_events_admin;Password={0};Database=eventsdb",
6 | "ConnectionString": "Host=localhost;Port=5432;Username=platform_events;Password={0};Database=eventsdb;Include Error Detail=True",
7 | "EventsDbAdminPwd": "Password",
8 | "EventsDbPwd": "Password"
9 | },
10 | "GeneralSettings": {
11 | "BaseUri": "http://localhost:5080",
12 | "OpenIdWellKnownEndpoint": "http://localhost:5101/authentication/api/v1/openid/",
13 | "JwtCookieName": "AltinnStudioRuntime"
14 | },
15 | "QueueStorageSettings": {
16 | "RegistrationQueueName": "events-registration",
17 | "InboundQueueName": "events-inbound",
18 | "OutboundQueueName": "events-outbound",
19 | "ValidationQueueName": "subscription-validation",
20 | "ConnectionString": "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1"
21 | },
22 | "PlatformSettings": {
23 | "RegisterApiBaseAddress": "http://localhost:5101/register/api/",
24 | "ApiProfileEndpoint": "http://localhost:5101/profile/api/v1/",
25 | "ApiAuthorizationEndpoint": "http://localhost:5101/authorization/api/v1/",
26 | "AppsDomain": "apps.altinn.no",
27 | "SubscriptionCachingLifetimeInSeconds": 3600,
28 | "SubscriptionKey": "inserted-during-deployment"
29 | }
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/src/Events/Exceptions/PlatformHttpException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using System.Threading.Tasks;
4 |
5 | namespace Altinn.Platform.Events.Exceptions
6 | {
7 | ///
8 | /// Exception class to hold exceptions when talking to the platform REST services
9 | ///
10 | public class PlatformHttpException : Exception
11 | {
12 | ///
13 | /// Responsible for holding an http request exception towards platform.
14 | ///
15 | public HttpResponseMessage Response { get; }
16 |
17 | ///
18 | /// Creates a platform exception
19 | ///
20 | /// The http response
21 | /// A PlatformHttpException
22 | public static async Task CreateAsync(HttpResponseMessage response)
23 | {
24 | string content = await response.Content.ReadAsStringAsync();
25 | string message = $"{(int)response.StatusCode} - {response.ReasonPhrase} - {content}";
26 |
27 | return new PlatformHttpException(response, message);
28 | }
29 |
30 | ///
31 | /// Copy the response for further investigations
32 | ///
33 | /// the response
34 | /// the message
35 | public PlatformHttpException(HttpResponseMessage response, string message) : base(message)
36 | {
37 | this.Response = response;
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Functions.Tests/IntegrationTests/TestableRetryBackoffService.cs:
--------------------------------------------------------------------------------
1 | using Altinn.Platform.Events.Functions.Queues;
2 | using Altinn.Platform.Events.Functions.Services;
3 | using Microsoft.Extensions.Logging;
4 |
5 | namespace Altinn.Platform.Events.Functions.Tests.IntegrationTests;
6 |
7 | ///
8 | /// Overrides the visibility timeout logic for integration tests.
9 | ///
10 | internal sealed class TestableRetryBackoffService: RetryBackoffService
11 | {
12 | public TestableRetryBackoffService(
13 | ILogger logger,
14 | QueueSendDelegate sendToQueue,
15 | PoisonQueueSendDelegate sendToPoison)
16 | : base(logger, sendToQueue, sendToPoison)
17 | {
18 | }
19 |
20 | ///
21 | /// Compressed visbility timeout based on dequeue count for integration tests.
22 | ///
23 | /// The number of times the event has been put back on the queue
24 | ///
25 | internal override TimeSpan GetVisibilityTimeout(int dequeueCount) => dequeueCount switch
26 | {
27 | 1 => TimeSpan.FromSeconds(1),
28 | 2 => TimeSpan.FromSeconds(2),
29 | 3 => TimeSpan.FromSeconds(3),
30 | 4 => TimeSpan.FromSeconds(4),
31 | 5 => TimeSpan.FromSeconds(5),
32 | 6 => TimeSpan.FromSeconds(6),
33 | 7 => TimeSpan.FromSeconds(7),
34 | 8 => TimeSpan.FromSeconds(8),
35 | 9 => TimeSpan.FromSeconds(9),
36 | 10 or 11 => TimeSpan.FromSeconds(10),
37 | _ => TimeSpan.FromSeconds(11)
38 | };
39 | }
40 |
--------------------------------------------------------------------------------
/src/Events.Functions/Queues/QueueSendDelegates.cs:
--------------------------------------------------------------------------------
1 | namespace Altinn.Platform.Events.Functions.Queues;
2 |
3 | ///
4 | /// Delegate for sending messages to a queue.
5 | ///
6 | /// The message content to send.
7 | /// Optional duration the message remains invisible in the queue after being added.
8 | /// Optional duration before the message expires in the queue.
9 | /// Token to cancel the operation.
10 | /// A task representing the asynchronous send operation.
11 | public delegate Task QueueSendDelegate(
12 | string message,
13 | TimeSpan? visibilityTimeout = null,
14 | TimeSpan? timeToLive = null,
15 | CancellationToken cancellationToken = default);
16 |
17 | ///
18 | /// Delegate for sending messages to a poison queue when they can't be processed normally.
19 | ///
20 | /// The message content to send to the poison queue.
21 | /// Optional duration the message remains invisible in the queue after being added.
22 | /// Optional duration before the message expires in the queue.
23 | /// Token to cancel the operation.
24 | /// A task representing the asynchronous send operation.
25 | public delegate Task PoisonQueueSendDelegate(
26 | string message,
27 | TimeSpan? visibilityTimeout = null,
28 | TimeSpan? timeToLive = null,
29 | CancellationToken cancellationToken = default);
30 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Mocks/Authentication/JwtCookiePostConfigureOptionsStub.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using AltinnCore.Authentication.JwtCookie;
3 |
4 | using Microsoft.AspNetCore.Authentication.Cookies;
5 | using Microsoft.Extensions.Options;
6 | using Microsoft.IdentityModel.Protocols;
7 | using Microsoft.IdentityModel.Protocols.OpenIdConnect;
8 |
9 | namespace Altinn.Platform.Events.Tests.Mocks.Authentication
10 | {
11 | ///
12 | /// Represents a stub for the class to be used in integration tests.
13 | ///
14 | public class JwtCookiePostConfigureOptionsStub : IPostConfigureOptions
15 | {
16 | ///
17 | public void PostConfigure(string name, JwtCookieOptions options)
18 | {
19 | if (string.IsNullOrEmpty(options.JwtCookieName))
20 | {
21 | options.JwtCookieName = JwtCookieDefaults.CookiePrefix + name;
22 | }
23 |
24 | if (options.CookieManager == null)
25 | {
26 | options.CookieManager = new ChunkingCookieManager();
27 | }
28 |
29 | if (!string.IsNullOrEmpty(options.MetadataAddress))
30 | {
31 | if (!options.MetadataAddress.EndsWith("/", StringComparison.Ordinal))
32 | {
33 | options.MetadataAddress += "/";
34 | }
35 | }
36 |
37 | options.MetadataAddress += ".well-known/openid-configuration";
38 | options.ConfigurationManager = new ConfigurationManagerStub();
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/test/k6/src/setup.js:
--------------------------------------------------------------------------------
1 | import * as tokenGenerator from "./api/token-generator.js";
2 | import * as maskinporten from "./api/maskinporten.js";
3 | import * as authentication from "./api/authentication.js";
4 | import { b64decode } from "k6/encoding";
5 |
6 | const environment = (__ENV.altinn_env || '').toLowerCase(); // Fallback value for when k6 inspect is run in script validation (env var evaluation yields 'undefined' in this phase)
7 |
8 | /*
9 | * generate an altinn token for org based on the environment using AltinnTestTools
10 | * If org is not provided TTD will be used.
11 | * @returns altinn token with the provided scopes for an org
12 | */
13 | export function getAltinnTokenForOrg(scopes, org = "ttd", orgNo = "991825827") {
14 | if ((environment == "prod" || environment == "tt02") && org == "ttd") {
15 | var accessToken = maskinporten.generateAccessToken(scopes);
16 | return authentication.exchangeToAltinnToken(accessToken, true);
17 | }
18 |
19 | var queryParams = {
20 | env: environment,
21 | scopes: scopes.replace(/ /gi, ","),
22 | org: org,
23 | orgNo: orgNo,
24 | };
25 |
26 | return tokenGenerator.generateEnterpriseToken(queryParams);
27 | }
28 |
29 | export function getAltinnTokenForUser() {
30 | if (environment == "prod" || environment == "tt02") {
31 | return authentication.authenticateUser();
32 | }
33 |
34 | return tokenGenerator.generatePersonalToken();
35 | }
36 |
37 | export function getPartyIdFromTokenClaim(jwtToken) {
38 | const parts = jwtToken.split(".");
39 | var claims = JSON.parse(b64decode(parts[1].toString(), "rawstd", "s"));
40 |
41 | return claims["urn:altinn:partyid"];
42 | }
43 |
--------------------------------------------------------------------------------
/src/Events/Migration/FunctionsAndProcedures/getappevents.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.getappevents_v2(
2 | _subject character varying,
3 | _after character varying,
4 | _from timestamp with time zone,
5 | _to timestamp with time zone,
6 | _type text[],
7 | _source text[],
8 | _resource text,
9 | _size integer)
10 | RETURNS TABLE(cloudevents text)
11 | LANGUAGE 'plpgsql'
12 | AS $BODY$
13 | DECLARE
14 | _sequenceno_first bigint;
15 | _sequenceno_last bigint;
16 | BEGIN
17 | IF _after IS NOT NULL AND _after <> '' THEN
18 | SELECT
19 | case count(*)
20 | when 0
21 | then 0
22 | else
23 | (SELECT MIN(sequenceno)
24 | FROM events.events
25 | WHERE cloudevent->>'id' = _after)
26 | end
27 | INTO _sequenceno_first
28 | FROM events.events
29 | WHERE cloudevent->>'id' = _after;
30 | END IF;
31 | SELECT MAX(sequenceno) INTO _sequenceno_last FROM events.events WHERE registeredtime <= now() - interval '30 second';
32 | return query
33 | SELECT cast(cloudevent as text) as cloudevents
34 | FROM events.events
35 | WHERE (_subject IS NULL OR cloudevent->>'subject' = _subject)
36 | AND (_from IS NULL OR cloudevent->>'time' >= to_json(_from)::text)
37 | AND (_to IS NULL OR cloudevent->>'time' <= to_json(_to)::text)
38 | AND (_type IS NULL OR cloudevent->>'type' ILIKE ANY(_type))
39 | AND (_source IS NULL OR cloudevent->>'source' ILIKE ANY(_source))
40 | AND (_resource IS NULL OR cloudevent->>'resource' = _resource)
41 | AND (_after IS NULL OR _after = '' OR sequenceno > _sequenceno_first)
42 | AND (_sequenceno_last IS NULL OR sequenceno <= _sequenceno_last)
43 | ORDER BY sequenceno
44 | limit _size;
45 | END;
46 | $BODY$;
47 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.40/01-create-function.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.getappevents_v2(
2 | _subject character varying,
3 | _after character varying,
4 | _from timestamp with time zone,
5 | _to timestamp with time zone,
6 | _type text[],
7 | _source text[],
8 | _resource text,
9 | _size integer)
10 | RETURNS TABLE(cloudevents text)
11 | LANGUAGE 'plpgsql'
12 | ROWS 1000
13 |
14 | AS $BODY$
15 | DECLARE
16 | _sequenceno_first bigint;
17 | _sequenceno_last bigint;
18 | BEGIN
19 | IF _after IS NOT NULL AND _after <> '' THEN
20 | SELECT
21 | case count(*)
22 | when 0
23 | then 0
24 | else
25 | (SELECT MIN(sequenceno)
26 | FROM events.events
27 | WHERE cloudevent->>'id' = _after)
28 | end
29 | INTO _sequenceno_first
30 | FROM events.events
31 | WHERE cloudevent->>'id' = _after;
32 | END IF;
33 | SELECT MAX(sequenceno) INTO _sequenceno_last FROM events.events WHERE registeredtime <= now() - interval '30 second';
34 | return query
35 | SELECT cast(cloudevent as text) as cloudevents
36 | FROM events.events
37 | WHERE (_subject IS NULL OR cloudevent->>'subject' = _subject)
38 | AND (_from IS NULL OR cloudevent->>'time' >= _from::text)
39 | AND (_to IS NULL OR cloudevent->>'time' <= _to::text)
40 | AND (_type IS NULL OR cloudevent->>'type' ILIKE ANY(_type))
41 | AND (_source IS NULL OR cloudevent->>'source' ILIKE ANY(_source))
42 | AND (_resource IS NULL OR cloudevent->>'resource' = _resource)
43 | AND (_after IS NULL OR _after = '' OR sequenceno > _sequenceno_first)
44 | AND (_sequenceno_last IS NULL OR sequenceno <= _sequenceno_last)
45 | ORDER BY sequenceno
46 | limit _size;
47 | END;
48 | $BODY$;
49 |
--------------------------------------------------------------------------------
/src/Events/Models/SubscriptionRequestModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 |
5 | namespace Altinn.Platform.Events.Models
6 | {
7 | ///
8 | /// Class that describes the events subscription request model
9 | ///
10 | public class SubscriptionRequestModel
11 | {
12 | ///
13 | /// Endpoint to receive matching events
14 | ///
15 | public Uri EndPoint { get; set; }
16 |
17 | ///
18 | /// Filter on source
19 | ///
20 | public Uri SourceFilter { get; set; }
21 |
22 | ///
23 | /// Filter on subject
24 | ///
25 | public string SubjectFilter { get; set; }
26 |
27 | ///
28 | /// Filter on resource
29 | ///
30 | public string ResourceFilter { get; set; }
31 |
32 | ///
33 | /// Filter on alternative subject
34 | ///
35 | public string AlternativeSubjectFilter { get; set; }
36 |
37 | ///
38 | /// Filter for type. The different sources has different types.
39 | ///
40 | public string TypeFilter { get; set; }
41 |
42 | ///
43 | /// Serializes the subscription request to a JSON string.
44 | ///
45 | /// Serialized cloud event
46 | public string Serialize()
47 | {
48 | return JsonSerializer.Serialize(this, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull });
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Events.Functions/Models/LogEntryDto.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 |
4 | namespace Altinn.Platform.Events.Functions.Models
5 | {
6 | ///
7 | /// Data transfer object for posting a log event after receiving a webhook response.
8 | ///
9 | public record LogEntryDto
10 | {
11 | ///
12 | /// The cloud event id associated with the logged event
13 | ///
14 | public string CloudEventId { get; set; }
15 |
16 | ///
17 | /// The resource associated with the cloud event
18 | ///
19 | public string CloudEventResource { get; set; }
20 |
21 | ///
22 | /// The type associated with the logged event
23 | ///
24 | public string CloudEventType { get; set; }
25 |
26 | ///
27 | /// The subscription id associated with the post action.
28 | ///
29 | public int SubscriptionId { get; set; }
30 |
31 | ///
32 | /// The consumer of the event
33 | ///
34 | public string Consumer { get; set; }
35 |
36 | ///
37 | /// The consumers webhook endpoint
38 | ///
39 | public Uri Endpoint { get; set; }
40 |
41 | ///
42 | /// The staus code returned from the subscriber endpoint
43 | ///
44 | public HttpStatusCode StatusCode { get; set; }
45 |
46 | ///
47 | /// Boolean value based on whether or not the response from the subscriber was successful
48 | ///
49 | public bool IsSuccessStatusCode { get; set; }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Events/Migration/v0.23/03-setup-function.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION events.insert_subscription(
2 | sourcefilter character varying,
3 | subjectfilter character varying,
4 | typefilter character varying,
5 | consumer character varying,
6 | endpointurl character varying,
7 | createdby character varying,
8 | validated boolean,
9 | sourcefilterhash character varying)
10 | RETURNS SETOF events.subscription
11 | LANGUAGE 'plpgsql'
12 | AS $BODY$
13 |
14 | DECLARE currentTime timestamptz;
15 |
16 | BEGIN
17 | SET TIME ZONE UTC;
18 | currentTime := NOW();
19 |
20 | RETURN QUERY
21 | INSERT INTO events.subscription(sourcefilter, subjectfilter, typefilter, consumer, endpointurl, createdby, "time", validated, sourcefilterhash)
22 | VALUES ($1, $2, $3, $4, $5, $6, currentTime, $7, $8) RETURNING *;
23 |
24 | END
25 | $BODY$;
26 |
27 |
28 |
29 |
30 | CREATE OR REPLACE FUNCTION events.getsubscriptions(
31 | sourcehashset character varying[],
32 | subject character varying,
33 | type character varying)
34 | RETURNS TABLE(id bigint, sourcefilter character varying, subjectfilter character varying, typefilter character varying, consumer character varying, endpointurl character varying, createdby character varying, validated boolean, "time" timestamp with time zone)
35 | LANGUAGE 'plpgsql'
36 | AS $BODY$
37 |
38 | BEGIN
39 | return query
40 | SELECT s.id, s.sourcefilter, s.subjectfilter, s.typefilter, s.consumer, s.endpointurl, s.createdby, s.validated, s."time"
41 | FROM events.subscription s
42 | WHERE s.sourcefilterhash = ANY(sourcehashset)
43 | AND (s.subjectfilter is NULL OR s.subjectfilter = subject)
44 | AND (s.typefilter is NULL OR s.typefilter = type)
45 | AND s.validated;
46 |
47 | END;
48 | $BODY$;
49 |
--------------------------------------------------------------------------------
/test/k6/src/apiHelpers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Build a string in a format of query param to the endpoint
3 | * @param {*} queryparams a json object with key as query name and value as query value
4 | * @example {"key1": "value1", "key2": "value2"}
5 | * @returns string a string like key1=value&key2=value2
6 | */
7 | export function buildQueryParametersForEndpoint(queryparams) {
8 | var query = "?";
9 | Object.keys(queryparams).forEach(function (key) {
10 | if (Array.isArray(queryparams[key])) {
11 | queryparams[key].forEach((value) => {
12 | query += key + "=" + value + "&";
13 | });
14 | } else {
15 | query += key + "=" + queryparams[key] + "&";
16 | }
17 | });
18 | query = query.slice(0, -1);
19 | return query;
20 | }
21 |
22 | export function buildHeaderWithBearer(token) {
23 | var params = {
24 | headers: {
25 | Authorization: "Bearer " + token
26 | }
27 | };
28 |
29 | return params;
30 | }
31 |
32 | export function buildHeaderWithBasic(token) {
33 | var params = {
34 | headers: {
35 | Authorization: "Basic " + token
36 | }
37 | };
38 |
39 | return params;
40 | }
41 |
42 | export function buildHeaderWithBearerAndContentType(token, contentType) {
43 | var params = {
44 | headers: {
45 | Authorization: "Bearer " + token,
46 | "Content-Type": contentType
47 | }
48 | };
49 |
50 | return params;
51 | }
52 |
53 | export function buildHeaderWithContentType(contentType) {
54 | var params = {
55 | headers: {
56 | "Content-Type": contentType
57 | }
58 | };
59 |
60 | return params;
61 | }
62 |
63 | export function buildHeaderWithCookie(name, value) {
64 | var params = {
65 | headers: {
66 | Cookie: name + "=" + value,
67 | }
68 | };
69 |
70 | return params;
71 | }
72 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Constants/XacmlRequestAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace Altinn.Platform.Storage.UnitTest.Constants
2 | {
3 | public static class XacmlRequestAttribute
4 | {
5 | ///
6 | /// xacml string that represents org
7 | ///
8 | public const string OrgAttribute = "urn:altinn:org";
9 |
10 | ///
11 | /// xacml string that represents app
12 | ///
13 | public const string AppAttribute = "urn:altinn:app";
14 |
15 | ///
16 | /// xacml string that represents isntance
17 | ///
18 | public const string InstanceAttribute = "urn:altinn:instance-id";
19 |
20 | ///
21 | /// xacm string that represents appresource
22 | ///
23 | public const string AppResourceAttribute = "urn:altinn:appresource";
24 |
25 | ///
26 | /// xacml string that represents task
27 | ///
28 | public const string TaskAttribute = "urn:altinn:task";
29 |
30 | ///
31 | /// xacml string that represents end event
32 | ///
33 | public const string EndEventAttribute = "urn:altinn:end-event";
34 |
35 | ///
36 | /// xacml string that represents party
37 | ///
38 | public const string PartyAttribute = "urn:altinn:partyid";
39 |
40 | ///
41 | /// xacml string that represents user
42 | ///
43 | public const string UserAttribute = "urn:altinn:userid";
44 |
45 | ///
46 | /// xacml string that represents role
47 | ///
48 | public const string RoleAttribute = "urn:altinn:rolecode";
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Events.Functions/Services/KeyVaultService.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.Security.Cryptography.X509Certificates;
3 |
4 | using Altinn.Platform.Events.Functions.Services.Interfaces;
5 |
6 | using Azure;
7 | using Azure.Identity;
8 | using Azure.Security.KeyVault.Certificates;
9 | using Microsoft.Extensions.Logging;
10 |
11 | namespace Altinn.Platform.Events.Functions.Services
12 | {
13 | ///
14 | /// Wrapper implementation for a KeyVaultClient. The wrapped client is created with a principal obtained through configuration.
15 | ///
16 | /// This class is excluded from code coverage because it has no logic to be tested.
17 | [ExcludeFromCodeCoverage]
18 | public class KeyVaultService : IKeyVaultService
19 | {
20 | ///
21 | public async Task GetCertificateAsync(string vaultUri, string secretId)
22 | {
23 | CertificateClient certificateClient = new(new Uri(vaultUri), new DefaultAzureCredential());
24 | AsyncPageable certificatePropertiesPage = certificateClient.GetPropertiesOfCertificateVersionsAsync(secretId);
25 | await foreach (CertificateProperties certificateProperties in certificatePropertiesPage)
26 | {
27 | if (certificateProperties.Enabled == true &&
28 | (certificateProperties.ExpiresOn == null || certificateProperties.ExpiresOn >= DateTime.UtcNow))
29 | {
30 | X509Certificate2 cert = await certificateClient.DownloadCertificateAsync(certificateProperties.Name, certificateProperties.Version);
31 | return cert;
32 | }
33 | }
34 |
35 | return null;
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Data/events/1.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "sequenceno": 1,
4 | "id": "e31dbb11-2208-4dda-a549-92a0db8c7708",
5 | "resource": "urn:altinn:resource:app_ttd_endring-av-navn-v2",
6 | "source": "https://ttd.apps.altinn.no/ttd/endring-av-navn-v2/instances/1337/6fb3f738-6800-4f29-9f3e-1c66862656cd",
7 | "subject": "/party/1337",
8 | "time": "2020-10-13T11:50:29Z",
9 | "type": "instance.deleted",
10 | "cloudEvent": {
11 | "id": "e31dbb11-2208-4dda-a549-92a0db8c7708",
12 | "resource": "urn:altinn:resource:app_ttd_endring-av-navn-v2",
13 |
14 | "source": "https://ttd.apps.altinn.no/ttd/endring-av-navn-v2/instances/1337/6fb3f738-6800-4f29-9f3e-1c66862656cd",
15 | "specversion": "1.0",
16 | "type": "instance.deleted",
17 | "subject": "/party/1337",
18 | "alternativesubject": "/person/01038712345",
19 | "time": "2020-10-13T11:50:29Z"
20 | }
21 | },
22 | {
23 | "sequenceno": 2,
24 | "id": "e31dbb11-2208-4dda-a549-92a0db8c8808",
25 | "resource": "urn:altinn:resource:app_ttd_endring-av-navn-v2",
26 | "source": "https://ttd.apps.altinn.no/ttd/endring-av-navn-v2/instances/1234324/6fb3f738-6800-4f29-9f3e-1c66862656cd",
27 | "subject": "/party/1337",
28 | "time": "2020-10-13T11:50:29Z",
29 | "type": "instance.restored",
30 | "cloudEvent": {
31 | "id": "e31dbb11-2208-4dda-a549-92a0db8c8808",
32 | "resource": "urn:altinn:resource:app_ttd_endring-av-navn-v2",
33 | "source": "https://ttd.apps.altinn.no/ttd/endring-av-navn-v2/instances/1234324/6fb3f738-6800-4f29-9f3e-1c66862656cd",
34 | "specversion": "1.0",
35 | "type": "instance.deleted",
36 | "subject": "/party/1337",
37 | "alternativesubject": "/person/01038712345",
38 | "time": "2020-10-13T12:50:29Z"
39 | }
40 | }
41 | ]
--------------------------------------------------------------------------------
/src/Events/Clients/Interfaces/IEventsQueueClient.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Altinn.Platform.Events.Models;
3 |
4 | namespace Altinn.Platform.Events.Clients.Interfaces
5 | {
6 | ///
7 | /// Describes the necessary methods for an implementation of an events queue client.
8 | ///
9 | public interface IEventsQueueClient
10 | {
11 | ///
12 | /// Enqueues the provided content to the registration queue
13 | ///
14 | /// The content to push to the queue in string format
15 | /// Returns a queue receipt
16 | public Task EnqueueRegistration(string content);
17 |
18 | ///
19 | /// Enqueues the provided content to the inbound queue
20 | ///
21 | /// The content to push to the queue in string format
22 | /// Returns a queue receipt
23 | public Task EnqueueInbound(string content);
24 |
25 | ///
26 | /// Enqueues the provided content to the outbound queue
27 | ///
28 | /// The content to push to the queue in string format
29 | /// Returns a queue receipt
30 | public Task EnqueueOutbound(string content);
31 |
32 | ///
33 | /// Enqueues the provided content to the validation queue
34 | ///
35 | /// The content to push to the validation queue in string format
36 | /// Returns a queue receipt
37 | public Task EnqueueSubscriptionValidation(string content);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Events/Services/Interfaces/ITraceLogService.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | using Altinn.Platform.Events.Models;
4 |
5 | using CloudNative.CloudEvents;
6 |
7 | namespace Altinn.Platform.Events.Services.Interfaces;
8 |
9 | ///
10 | /// Interface for trace log service.
11 | ///
12 | public interface ITraceLogService
13 | {
14 | ///
15 | /// Creates a trace log entry based on registration of a new event
16 | ///
17 | /// CloudNative CloudEvent
18 | /// A representing the result of the asynchronous operation: Cloud event id.
19 | Task CreateRegisteredEntry(CloudEvent cloudEvent);
20 |
21 | ///
22 | /// Log response from webhook post to subscriber.
23 | ///
24 | /// Data transfer object associated with cloud event, status code, and subscription
25 | /// A string representation of the GUID or an empty string
26 | Task CreateWebhookResponseEntry(LogEntryDto logEntryDto);
27 |
28 | ///
29 | /// Creates a trace log entry with information about cloud event and subscription
30 | ///
31 | /// Cloud Event associated with log entry
32 | /// Subscription associated with log entry
33 | /// Type of activity associated with log entry
34 | /// Returns empty string if a log entry can't be created
35 | Task CreateLogEntryWithSubscriptionDetails(CloudEvent cloudEvent, Subscription subscription, TraceLogActivity activity);
36 | }
37 |
--------------------------------------------------------------------------------
/src/Events/Extensions/CloudEventExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | using CloudNative.CloudEvents;
4 | using CloudNative.CloudEvents.SystemTextJson;
5 |
6 | namespace Altinn.Platform.Events.Extensions
7 | {
8 | ///
9 | /// Extension methods for cloud events
10 | ///
11 | public static class CloudEventExtensions
12 | {
13 | ///
14 | /// Serializes the cloud event using a JsonEventFormatter
15 | ///
16 | /// The json serialized cloud event
17 | public static string Serialize(this CloudEvent cloudEvent, CloudEventFormatter formatter = null)
18 | {
19 | formatter ??= new JsonEventFormatter();
20 | var bytes = formatter.EncodeStructuredModeMessage(cloudEvent, out _);
21 | return Encoding.UTF8.GetString(bytes.Span);
22 | }
23 |
24 | ///
25 | /// Retrieves the resource from the cloud event. Returns null if it isn't defined.
26 | ///
27 | public static string GetResource(this CloudEvent cloudEvent) => cloudEvent["resource"]?.ToString();
28 |
29 | ///
30 | /// Retrieves the resource instance from the cloud event. Returns null if it isn't defined.
31 | ///
32 | public static string GetResourceInstance(this CloudEvent cloudEvent) => cloudEvent["resourceinstance"]?.ToString();
33 |
34 | ///
35 | /// Sets the resource extension attribute of the cloud event if it is undefiend
36 | ///
37 | public static void SetResourceIfNotDefined(this CloudEvent cloudEvent, string resource)
38 | {
39 | if (cloudEvent.GetResource() == null)
40 | {
41 | cloudEvent["resource"] = resource;
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Tests/Mocks/EventsQueueClientMock.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text.Json;
3 | using System.Threading.Tasks;
4 | using Altinn.Platform.Events.Clients.Interfaces;
5 | using Altinn.Platform.Events.Models;
6 |
7 | namespace Altinn.Platform.Events.Tests.Mocks
8 | {
9 | public class EventsQueueClientMock : IEventsQueueClient
10 | {
11 | public EventsQueueClientMock()
12 | {
13 | OutboundQueue = new Dictionary>();
14 | }
15 |
16 | ///
17 | /// Queue mock for unit test
18 | ///
19 | public Dictionary> OutboundQueue { get; set; }
20 |
21 | public Task EnqueueRegistration(string content)
22 | {
23 | return Task.FromResult(new QueuePostReceipt { Success = true });
24 | }
25 |
26 | public Task EnqueueInbound(string content)
27 | {
28 | return Task.FromResult(new QueuePostReceipt { Success = true });
29 | }
30 |
31 | public Task EnqueueOutbound(string content)
32 | {
33 | CloudEventEnvelope cloudEventEnvelope = JsonSerializer.Deserialize(content);
34 |
35 | var hash = cloudEventEnvelope.CloudEvent.GetHashCode();
36 | if (!OutboundQueue.ContainsKey(hash))
37 | {
38 | OutboundQueue.Add(hash, new List());
39 | }
40 |
41 | OutboundQueue[hash].Add(cloudEventEnvelope);
42 |
43 | return Task.FromResult(new QueuePostReceipt { Success = true });
44 | }
45 |
46 | public Task EnqueueSubscriptionValidation(string content)
47 | {
48 | return Task.FromResult(new QueuePostReceipt { Success = true });
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Events/Models/LogEntryDto.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 | using System;
3 | using System.Net;
4 |
5 | using CloudNative.CloudEvents;
6 |
7 | namespace Altinn.Platform.Events.Models
8 | {
9 | ///
10 | /// Data transfer object for posting a log event after receiving a webhook response.
11 | ///
12 | public record LogEntryDto
13 | {
14 | ///
15 | /// The cloud event id associated with the logged event "/>
16 | ///
17 | public string? CloudEventId { get; set; }
18 |
19 | ///
20 | /// The resource associated with the cloud event
21 | ///
22 | public string? CloudEventResource { get; set; }
23 |
24 | ///
25 | /// The type associated with the logged event
26 | ///
27 | public string? CloudEventType { get; set; }
28 |
29 | ///
30 | /// The subscription id associated with the post action. "/>
31 | ///
32 | public int? SubscriptionId { get; set; }
33 |
34 | ///
35 | /// The consumer of the event
36 | ///
37 | public string? Consumer { get; set; }
38 |
39 | ///
40 | /// The consumers webhook endpoint
41 | ///
42 | public Uri? Endpoint { get; set; }
43 |
44 | ///
45 | /// The staus code returned from the subscriber endpoint
46 | ///
47 | public HttpStatusCode? StatusCode { get; set; }
48 |
49 | ///
50 | /// Boolean value based on whether or not the response from the subscriber was successful
51 | ///
52 | public bool? IsSuccessStatusCode { get; set; } = false;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: 'CodeQL'
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths:
7 | - 'src/**'
8 | - '.github/workflows/**'
9 | pull_request:
10 | branches: [main]
11 | types: [opened, synchronize, reopened]
12 | paths:
13 | - 'src/**'
14 | - '.github/workflows/**'
15 | schedule:
16 | - cron: '18 22 * * 3'
17 |
18 | jobs:
19 | analyze:
20 | name: Analyze
21 | runs-on: ubuntu-latest
22 | permissions:
23 | actions: read
24 | contents: read
25 | security-events: write
26 |
27 | strategy:
28 | fail-fast: false
29 | matrix:
30 | include:
31 | - language: actions
32 | build-mode: none
33 | - language: csharp
34 | build-mode: autobuild
35 | steps:
36 | - name: Checkout repository
37 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
38 | - name: Setup .NET
39 | if: matrix.language == 'csharp'
40 | uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
41 | with:
42 | dotnet-version: 10.0.x
43 | - name: Initialize CodeQL
44 | uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
45 | with:
46 | languages: ${{ matrix.language }}
47 | build-mode: ${{ matrix.build-mode }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
52 |
53 | - name: Perform CodeQL Analysis
54 | uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
55 | with:
56 | category: '/language:${{matrix.language}}'
57 |
--------------------------------------------------------------------------------
/src/Events.Functions/Clients/Interfaces/IEventsClient.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | using Altinn.Platform.Events.Functions.Models;
4 |
5 | using CloudNative.CloudEvents;
6 |
7 | namespace Altinn.Platform.Events.Functions.Clients.Interfaces
8 | {
9 | ///
10 | /// Interface to Events Inbound queue API
11 | ///
12 | public interface IEventsClient
13 | {
14 | ///
15 | /// Stores a cloud event document to the events database.
16 | ///
17 | /// The CloudEvent to be stored
18 | /// CloudEvent as stored in the database
19 | Task SaveCloudEvent(CloudEvent cloudEvent);
20 |
21 | ///
22 | /// Posts an event with the given statusCode
23 | ///
24 | /// Wrapper object for cloud event and subscriber data
25 | /// Http status code returned
26 | /// Boolean value that indicates whether the status code was successful or not
27 | ///
28 | Task LogWebhookHttpStatusCode(CloudEventEnvelope cloudEventEnvelope, System.Net.HttpStatusCode statusCode, bool isSuccessStatusCode);
29 |
30 | ///
31 | /// Send cloudEvent for outbound processing.
32 | ///
33 | /// CloudEvent to send
34 | Task PostInbound(CloudEvent cloudEvent);
35 |
36 | ///
37 | /// Send cloudEvent for outbound processing.
38 | ///
39 | /// CloudEvent to send
40 | Task PostOutbound(CloudEvent cloudEvent);
41 |
42 | ///
43 | /// Set a subscription as valid
44 | ///
45 | public Task ValidateSubscription(int subscriptionId);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Events/Controllers/LogsController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Altinn.Platform.Events.Configuration;
4 | using Altinn.Platform.Events.Models;
5 | using Altinn.Platform.Events.Services.Interfaces;
6 |
7 | using Microsoft.AspNetCore.Authorization;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.AspNetCore.Mvc;
10 | using Swashbuckle.AspNetCore.Annotations;
11 |
12 | namespace Altinn.Platform.Events.Controllers
13 | {
14 | ///
15 | /// Controller for logging event operations to persistence
16 | ///
17 | /// Service for logging event operations to persistence
18 | [Route("events/api/v1/storage/events/logs")]
19 | [ApiController]
20 | [SwaggerTag("Private API")]
21 | public class LogsController(ITraceLogService traceLogService) : ControllerBase
22 | {
23 | private readonly ITraceLogService _traceLogService = traceLogService;
24 |
25 | ///
26 | /// Create a new trace log for cloud event with a status code.
27 | ///
28 | /// The event wrapper associated with the event for logging
29 | ///
30 | [Authorize(Policy = AuthorizationConstants.POLICY_PLATFORM_ACCESS)]
31 | [HttpPost]
32 | [Consumes("application/json")]
33 | [SwaggerResponse(201, Type = typeof(Guid))]
34 | [ProducesResponseType(StatusCodes.Status401Unauthorized)]
35 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
36 | public async Task Logs([FromBody] LogEntryDto logEntry)
37 | {
38 | var result = await _traceLogService.CreateWebhookResponseEntry(logEntry);
39 |
40 | if (string.IsNullOrEmpty(result))
41 | {
42 | return BadRequest();
43 | }
44 |
45 | return Created();
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Events.Functions/Program.cs:
--------------------------------------------------------------------------------
1 | using Altinn.Common.AccessTokenClient.Services;
2 | using Altinn.Platform.Events.Functions;
3 | using Altinn.Platform.Events.Functions.Clients;
4 | using Altinn.Platform.Events.Functions.Clients.Interfaces;
5 | using Altinn.Platform.Events.Functions.Configuration;
6 | using Altinn.Platform.Events.Functions.Services;
7 | using Altinn.Platform.Events.Functions.Services.Interfaces;
8 |
9 | using Microsoft.ApplicationInsights.Extensibility;
10 | using Microsoft.Azure.Functions.Worker;
11 | using Microsoft.Azure.Functions.Worker.Builder;
12 | using Microsoft.Extensions.DependencyInjection;
13 | using Microsoft.Extensions.Hosting;
14 |
15 | var builder = FunctionsApplication.CreateBuilder(args);
16 |
17 | builder.ConfigureFunctionsWebApplication();
18 |
19 | builder.Services.AddApplicationInsightsTelemetryWorkerService();
20 | builder.Services.ConfigureFunctionsApplicationInsights();
21 |
22 | builder.Services.Configure(builder.Configuration.GetSection("Platform"));
23 | builder.Services.Configure(builder.Configuration.GetSection("KeyVault"));
24 | builder.Services.Configure(builder.Configuration.GetSection("CertificateResolver"));
25 | builder.Services.Configure(builder.Configuration.GetSection("EventsOutboundSettings"));
26 |
27 | builder.Services.AddSingleton();
28 |
29 | builder.Services.AddSingleton();
30 | builder.Services.AddSingleton();
31 | builder.Services.AddSingleton();
32 | builder.Services.AddHttpClient();
33 | builder.Services.AddHttpClient();
34 |
35 | builder.Services.AddQueueSenders(builder.Configuration);
36 | builder.Services.AddTransient();
37 |
38 | await builder.Build().RunAsync();
39 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Functions.Tests/TestingServices/platform-org.pfx:
--------------------------------------------------------------------------------
1 | MIIF1jCCA76gAwIBAgIQe/UpvwBNvG5aCRa+6QEZqzANBgkqhkiG9w0BAQsFADANMQswCQYDVQQDEwJjYTAeFw0xODAzMTcxODM5NTFaFw0yMDAzMTYxODM5NTFaMBQxEjAQBgNVBAMTCWFwaXNlcnZlcjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMWTr7hiQPr1csDR/OpAbR/tkNGN4mCOPlQx+xao+JQlPP1Uo66X9oc3vrCBPZG19FWodU5zRe2+ql279zmMLgwKIGOsYuDmAV7m9Gg9tNG5nB9WHhD7WX0gOBpPW0csJTDzlr9FkJcmMKXNFOYeydhkn2lrr+0uCumI4AC3j/ACRls7J7EV1Q09HyHdL+5q4eAr92AUs217PpWWAMqMFo4WC/4NuqEfnMR2jpPzDoIJBDxt3NljiaRRS2LfB34O4aCKCis2jlMjYTahKIXDDv1pWW67AsGuGBpgdb2iYRUMze4NIUqQZrVGhnDcnRcbsfsldbBEoZBfLEaUm0hgSJUNnX3K1Adv7lHGxbdk/m9M1YEjb1EAX7rKMPg2uKcHeVv74Xwa0+cvke8ErYg3iSuuLPQ4qzPTV6LcdCmfbBsIyUiVJpCaa4RX8uzRXHAx1EvN2k7iZQutdrT2Sgj+4cG9E33hZM2AsOJuyXZMMVMtUveOQeth8iQNcT4FDwqc1WZDnpVMlqpTnDzTIAlrN+5WzJgzIj6GTsILyKC91GuI5jSrCExjwUB6D4oGPA5X/eOiNU1yUFNouYSCnAun9D+RSVfBWEVAVumCRbRcsOmBIE6MwpCZCHzXhAUBvMzk9/qhVDxTuaBt+Nf4WHopAS0KFsJGVee1tsva4zI34HgLAgMBAAGjggEpMIIBJTAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADCB7wYDVR0RBIHnMIHkgg5oY3Ata3ViZXJuZXRlc4IKa3ViZXJuZXRlc4IWa3ViZXJuZXRlcy5kZWZhdWx0LnN2Y4Ika3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsgjloY3Ata3ViZXJuZXRlcy41YWFkNjBmMTg5YjU0NTAwMDEyYTQ0MDEuc3ZjLmNsdXN0ZXIubG9jYWyCQXNhYXJzLWFrcy0tc2FhcnMtcGxheS1yZXNvdS1lOGVhNGUtNGFkNmQyOTMuaGNwLndlc3R1czIuYXptazhzLmlvhwQKAAABhwQKAAABMA0GCSqGSIb3DQEBCwUAA4ICAQDHG4mm3iIOxzirvNX9SZn0G26Zt/h4z3k07mMKUHB9jmYbtqWqQX1LfocZs+s6/02q88ilwATFJg1Qv5NkW7QsfreSCbyOq/9JLMEiQlbddjkt/U8czUU0kGLn+0m758XkPkwRgPIiMz437YhlfmpVI5gv63QfxfnRqrK2WqmoO6RMmaWc2aZFoVL521KxX0pp+3vAE9AwfvWpNgJkTirVgNhe6QL1tfA0RVllGfil3Re1yAQaBYD3mIBtiFvTML/Zm3GjxJXtXqT7JtM4bibHqhKywjgx1rcDa1WOLta51mfGiqOMOP/sdXtKcs/zdIMZOie6mOh8ZNfHdGOdCrNbTj8fL3OtwlzJGFPuWwAYJjT8Fcudg6zCZ6CuK26tz3rJ7665NXVdS+ljAA2Pfl6MefhhYL4RUSWEtFCqNqeWgyRzWvQcVasTX7k8lptY8yLPO3c636UMvfESFQqVZpC6xv66c5jBarKeCUmRCjmtXqVgGtEQCDk7hVp1A9nxmpi4S0Ubg4bQAPIdkQeR4uj2Jiwu5a4sKQHV1LxDovWde15CuofMvzIswPJfMdM5TiOFGtd6vhFjcOGCvM370IrLS/tg8+vNuocx+orueX7vjHwYL3IBlrZctiRAAOklVoQfVNH/aY0cfbSvqTX3edTtT/h7GJuzVtfccpCvyw5pnw==
--------------------------------------------------------------------------------
/src/Events/Models/TraceLog.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 | using System;
3 |
4 | using CloudNative.CloudEvents;
5 |
6 | namespace Altinn.Platform.Events.Models
7 | {
8 | ///
9 | /// Class that describes a trace log entry to be persisted
10 | ///
11 | public class TraceLog
12 | {
13 | ///
14 | /// Gets or sets the unique identifier for the cloud event.
15 | ///
16 | public Guid? CloudEventId { get; set; }
17 |
18 | ///
19 | /// Gets or sets the resource associated with the trace log entry.
20 | ///
21 | public string? Resource { get; set; } = default!;
22 |
23 | ///
24 | /// Gets or sets the type of event being logged.
25 | ///
26 | public string? EventType { get; set; } = default!;
27 |
28 | ///
29 | /// Gets or sets the consumer associated with the trace log entry.
30 | ///
31 | public string? Consumer { get; set; }
32 |
33 | ///
34 | /// Gets or sets the endpoint of the subscriber. Used with webhook calls.
35 | ///
36 | public string? SubscriberEndpoint { get; set; }
37 |
38 | ///
39 | /// Reference to the subscription that this trace log entry is associated with. Can be null
40 | ///
41 | public int? SubscriptionId { get; set; } = default!;
42 |
43 | ///
44 | /// Gets or sets the response code returned by the subscriber. Should be a valid HTTP status code.
45 | ///
46 | public int? ResponseCode { get; set; }
47 |
48 | ///
49 | /// Gets or sets the activity associated with the trace log entry.
50 | ///
51 | public TraceLogActivity Activity { get; set; }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/test/Altinn.Platform.Events.Functions.Tests/Altinn.Platform.Events.Functions.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net10.0
5 | enable
6 |
7 | false
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | all
18 | runtime; build; native; contentfiles; analyzers; buildtransitive
19 |
20 |
21 | all
22 | runtime; build; native; contentfiles; analyzers; buildtransitive
23 |
24 |
25 |
26 |
27 |
28 | all
29 | runtime; build; native; contentfiles; analyzers; buildtransitive
30 |
31 |
32 | stylecop.json
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | true
43 | $(NoWarn);1591
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/test/k6/readme.md:
--------------------------------------------------------------------------------
1 | ## k6 test project for automated tests
2 |
3 | # Getting started
4 |
5 |
6 | Install pre-requisites
7 | ## Install k6
8 |
9 | *We recommend running the tests through a docker container.*
10 |
11 | From the command line:
12 |
13 | > docker pull grafana/k6
14 |
15 |
16 | Further information on [installing k6 for running in docker is available here.](https://k6.io/docs/get-started/installation/#docker)
17 |
18 |
19 | Alternatively, it is possible to run the tests directly on your machine as well.
20 |
21 | [General installation instructions are available here.](https://k6.io/docs/get-started/installation/)
22 |
23 |
24 | ## Running tests
25 |
26 | All tests are defined in `src/tests` and in the top of each test file an example of the cmd to run the test is available.
27 |
28 | The command should be run from the root of the k6 folder.
29 |
30 | >$> cd /altinn-events/test/k6
31 |
32 | Run test suite by specifying filename.
33 |
34 | For example:
35 |
36 | >$> podman compose run k6 run /src/tests/events/post.js -e tokenGeneratorUserName=*** -e tokenGeneratorUserPwd=*** -e env=***
37 |
38 | The command consists of three sections
39 |
40 | `podman compose run` to run the test in a docker container
41 |
42 | `k6 run {path to test file}` pointing to the test file you want to run e.g. `/src/tests/events/post.js`
43 |
44 |
45 | `-e tokenGeneratorUserName=*** -e tokenGeneratorUserPwd=*** -e env=***` all environment variables that should be included in the request.
46 |
47 |
48 | ### Webhook for subscriptions
49 |
50 | When testing the subscriptions a webhook must be provided.
51 | You are free to provide whichever endpoint, but make sure it ends with `/`.
52 |
53 | We would suggest to use webhook.site.
54 | The following PowerShell script will generate a dedicated webhook to provide as the environment variable `webhookEndpoint`
55 |
56 |
57 | ```ps
58 | $params = @{
59 | Uri = "https://webhook.site/token"
60 | Method = "Post"
61 | }
62 |
63 | $webhookToken =((Invoke-Webrequest @params).Content | ConvertFrom-Json).uuid
64 |
65 | $webhookEndpoint= "https://webhook.site/" + $webhookToken + "/"
66 | ```
67 |
--------------------------------------------------------------------------------
/src/Events/Models/CloudEventEnvelope.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.Json;
3 |
4 | using Altinn.Platform.Events.Extensions;
5 | using CloudNative.CloudEvents;
6 |
7 | namespace Altinn.Platform.Events.Models
8 | {
9 | ///
10 | /// Outbound processing state object
11 | ///
12 | public class CloudEventEnvelope
13 | {
14 | ///
15 | /// The Event to push
16 | ///
17 | public CloudEvent CloudEvent { get; set; }
18 |
19 | ///
20 | /// The time the event was posted to the outbound queue
21 | ///
22 | public DateTime Pushed { get; set; }
23 |
24 | ///
25 | /// Target URI to push event
26 | ///
27 | public Uri Endpoint { get; set; }
28 |
29 | ///
30 | /// The consumer of the events
31 | ///
32 | public string Consumer { get; set; }
33 |
34 | ///
35 | /// The subscription id that matched.
36 | ///
37 | public int SubscriptionId { get; set; }
38 |
39 | ///
40 | /// CloudEvent property requires specialized serialization handling.
41 | /// Uses string manipulation to insert the serialized cloud event.
42 | ///
43 | /// A json serialized cloud envelope
44 | public string Serialize()
45 | {
46 | var cloudEvent = CloudEvent;
47 | string serializedCloudEvent = CloudEvent.Serialize();
48 | CloudEvent = null;
49 |
50 | var partalSerializedEnvelope = JsonSerializer.Serialize(this, new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull });
51 | var index = partalSerializedEnvelope.LastIndexOf('}');
52 | string serializedEnvelope = partalSerializedEnvelope.Insert(index, $", \"CloudEvent\":{serializedCloudEvent}");
53 |
54 | CloudEvent = cloudEvent;
55 | return serializedEnvelope;
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------