├── .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 | [![Events scan](https://github.com/altinn/altinn-events/actions/workflows/events-scan.yml/badge.svg)](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 | --------------------------------------------------------------------------------