├── version.txt ├── .github ├── CODEOWNERS ├── workflows │ ├── test-use-cases-PROD.yml │ ├── test-use-cases-TT02.yml │ ├── validate-formatting.yml │ ├── check-label-for-pr.yml │ ├── test-application.yml │ └── ci-cd.yaml ├── release.yml ├── pull_request_template.md ├── ISSUE_TEMPLATE │ ├── question.yml │ ├── epic--new-template-.md │ ├── feature_request.yml │ ├── bug_report.yml │ ├── chore.yml │ ├── analysis.yml │ ├── user_story.yml │ └── epic.yml ├── actions │ ├── publish-image │ │ └── action.yml │ ├── get-current-version │ │ └── action.yml │ ├── release-to-git │ │ └── action.yml │ └── remediate-standard-tags │ │ └── action.yml └── tools │ ├── revisionVerifier.sh │ └── pwdGenerator.ps1 ├── tests ├── Altinn.Broker.LoadTests │ ├── data │ │ └── testfile.txt │ └── docker-compose-loadtest.yml ├── Altinn.Broker.Tests │ ├── Data │ │ ├── ManifestFileTests │ │ │ ├── payload.txt │ │ │ ├── Empty.zip │ │ │ ├── Manifest.xml │ │ │ ├── Payload.zip │ │ │ └── PayloadWithExistingManifest.zip │ │ ├── postgresql.conf │ │ ├── WebHookSubscriptionValidationTest.json │ │ ├── MalwareScanResult_NoThreatFound.json │ │ ├── MalwareScanResult_Malicious.json │ │ ├── altinn-broker-test-resource-1.json │ │ └── R__Prepare_Test_Data.sql │ ├── ServiceOwnerControllerTests.cs │ └── Factories │ │ ├── FileTransferEntityFactory.cs │ │ └── FileTransferInitializeExtTestFactory.cs ├── Altinn.Broker.UseCaseTests │ ├── fixtures │ │ ├── usecase-broker-test-file.txt │ │ └── usecase-broker-test-file2.txt │ └── helpers │ │ ├── commonUtils.js │ │ ├── cryptoUtils.js │ │ ├── cleanupUseCaseTestsData.js │ │ ├── brokerPayloadBuilder.js │ │ ├── maskinportenTokenService.js │ │ ├── maskinportenJwtBuilder.js │ │ └── altinnTokenService.js └── Altinn.Broker.Tests.LargeFile │ ├── Altinn.Broker.Tests.LargeFile.csproj │ └── Dockerfile ├── .bruno ├── Resource │ ├── folder.bru │ ├── Get resource configuration.bru │ └── Configure resource for Broker.bru ├── FileTransfer │ ├── folder.bru │ ├── {fileTransferId} │ │ ├── Overview.bru │ │ ├── Download.bru │ │ ├── Upload.bru │ │ ├── Confirm Download.bru │ │ └── Details.bru │ ├── Search.bru │ ├── Initialize and upload (form-data).bru │ └── Initialize.bru ├── ServiceOwner │ ├── folder.bru │ ├── Configure service owner for Broker.bru │ └── Get service owner configuration.bru ├── Authentication │ ├── folder.bru │ ├── Exchange sender token for Altinn token.bru │ ├── Exchange recipient token for Altinn token.bru │ ├── Exchange systemprovider token for Altinn token.bru │ ├── Get Systemprovider Maskinporten Token.bru │ ├── Create sender system user Maskinporten token.bru │ └── Create recipient system user maskinporten token.bru ├── SystemRegister │ ├── folder.bru │ ├── System Overview.bru │ ├── Create sender systemuser request.bru │ ├── Create recipient systemuser request.bru │ └── Register system.bru ├── bruno.json └── jwt-helper.js ├── start.ps1 ├── src ├── Altinn.Broker.Persistence │ ├── Migrations │ │ ├── V0004__add_hangfire_job_id.sql │ │ ├── V0008__manifest_shim.sql │ │ ├── V0013__optimize_legacy_search3.sql │ │ ├── V0003__idempotency_event.sql │ │ ├── V0006__add_party_id.sql │ │ ├── V0007__add_graceful_ttl.sql │ │ ├── V0012__optimize_legacy_search2.sql │ │ ├── V0017__remove_property_length_limit.sql │ │ ├── V0019__cascade_all_tables.sql │ │ ├── V0009__add_external_service_code.sql │ │ ├── V0005__add_resource_table.sql │ │ ├── V0016__activate_cron.sql │ │ ├── V0002__hangfire.sql │ │ ├── V0015__new_legacy_search.sql │ │ ├── V0014__search_indices.sql │ │ ├── V0001__enum_tables.sql │ │ ├── V0010__toggleable_virusscan.sql │ │ ├── V0011__optimize_legacy_search.sql │ │ └── V0018__denormalized_file_transfer_status.sql │ ├── Options │ │ └── DatabaseOptions.cs │ ├── Altinn.Broker.Persistence.csproj │ └── Repositories │ │ └── IdempotencyEventRepository.cs ├── Altinn.Broker.API │ ├── Enums │ │ ├── RoleExt.cs │ │ ├── LegacyRecipientFileStatusExt.cs │ │ ├── RecipientFileTransferStatusExt.cs │ │ ├── DeploymentStatusExt.cs │ │ ├── LegacyFileStatusExt.cs │ │ └── FileTransferStatusExt.cs │ ├── Models │ │ ├── Maskinporten │ │ │ ├── MaskinportenSecurityTokenException.cs │ │ │ ├── MaskinportenConsumer.cs │ │ │ └── MaskinportenSupplier.cs │ │ ├── FileTransferUploadResponseExt.cs │ │ ├── FileTransferInitializeResponseExt.cs │ │ ├── ServiceOwner │ │ │ ├── ServiceOwnerInitializeExt.cs │ │ │ └── ServiceOwnerOverviewExt.cs │ │ ├── Recipient │ │ │ ├── LegacyRecipientFileStatusEventExt.cs │ │ │ ├── LegacyRecipientFileStatusDetailsExt.cs │ │ │ ├── RecipientFileStatusEventExt.cs │ │ │ └── RecipientFileStatusDetailsExt.cs │ │ ├── FileTransferInitializeAndUploadExt.cs │ │ ├── FileTransferStatusEventExt.cs │ │ └── FileTransferStatusDetailsExt.cs │ ├── appsettings.json │ ├── Configuration │ │ ├── Constants.cs │ │ └── AuthorizationConstants.cs │ ├── Helpers │ │ ├── SecurityHeadersMiddleware.cs │ │ ├── ValidateElementsInList.cs │ │ ├── LogContextHelpers.cs │ │ ├── AltinnTokenEventsHelper.cs │ │ ├── MaskinportenHelper.cs │ │ └── ValidateUseManifestFileShim.cs │ ├── Mappers │ │ ├── LegacyInitializeFileMapper.cs │ │ └── InitializeFileTransferMapper.cs │ ├── ValidationAttributes │ │ ├── ResourceIdentifierAttribute.cs │ │ ├── MD5ChecksumAttribute.cs │ │ └── PropertyListAttribute.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Controllers │ │ └── HealthController.cs │ ├── appsettings.Development.json │ └── Swagger │ │ └── BinaryRequestBodyOperationFilter.cs ├── Altinn.Broker.Core │ ├── Domain │ │ ├── Enums │ │ │ ├── StorageProviderType.cs │ │ │ ├── ResourceAccessLevel.cs │ │ │ ├── SearchRole.cs │ │ │ ├── ActorFileStatus.cs │ │ │ ├── DeploymentStatus.cs │ │ │ ├── FileTransferStatus.cs │ │ │ ├── AltinnVersion.cs │ │ │ └── RecipientType.cs │ │ ├── ActorEntity.cs │ │ ├── PartyEntity.cs │ │ ├── FileTransferStatusEntity.cs │ │ ├── Webhooks │ │ │ └── MalwareScan │ │ │ │ ├── ScanResultDetails.cs │ │ │ │ ├── ScanResultData.cs │ │ │ │ └── EventMessage.cs │ │ ├── ActorFileTransferStatusEntity.cs │ │ ├── StorageProviderEntity.cs │ │ ├── FileTransferSearchEntity.cs │ │ ├── ServiceOwnerEntity.cs │ │ ├── ResourceEntity.cs │ │ ├── LegacyFileSearchEntity.cs │ │ └── FileTransferEntity.cs │ ├── Services │ │ ├── Enums │ │ │ ├── AltinnEventSubjectRole.cs │ │ │ └── AltinnEventType.cs │ │ ├── IAltinnRegisterService.cs │ │ ├── IEventBus.cs │ │ ├── IResourceManager.cs │ │ └── IBrokerStorageService.cs │ ├── Options │ │ ├── ReportStorageOptions.cs │ │ ├── GeneralSettings.cs │ │ ├── AltinnOptions.cs │ │ └── SlackSettings.cs │ ├── Helpers │ │ ├── SanitizerMethods.cs │ │ ├── IManifestDownloadStream.cs │ │ ├── FileTransferExtensions.cs │ │ └── TransactionWithRetriesPolicy.cs │ ├── Repositories │ │ ├── IIdempotencyEventRepository.cs │ │ ├── IPartyRepository.cs │ │ ├── IActorRepository.cs │ │ ├── IServiceOwnerRepository.cs │ │ ├── IActorFileTransferStatusRepository.cs │ │ ├── IAltinnResourceRepoistory.cs │ │ ├── IFileTransferStatusRepository.cs │ │ ├── IAuthorizationService.cs │ │ └── IResourceRepository.cs │ └── Altinn.Broker.Core.csproj ├── Altinn.Broker.Application │ ├── CleanupUseCaseTests │ │ ├── CleanupUseCaseTestsRequest.cs │ │ └── CleanupUseCaseTestsResponse.cs │ ├── GetFileTransferDetails │ │ ├── GetFileTransferDetailsRequest.cs │ │ └── GetFileTransferDetailsResponse.cs │ ├── DownloadFile │ │ ├── DownloadFileResponse.cs │ │ └── DownloadFileRequest.cs │ ├── ConfirmDownload │ │ └── ConfirmDownloadRequest.cs │ ├── GetFileTransferOverview │ │ ├── GetFileTransferOverviewsResponse.cs │ │ ├── GetFileTransferOverviewResponse.cs │ │ └── GetFileTransferOverviewRequest.cs │ ├── UploadFile │ │ └── UploadFileRequest.cs │ ├── ApplicationConstants.cs │ ├── PurgeFileTransfer │ │ └── PurgeFileTransferRequest.cs │ ├── IHandler.cs │ ├── GetFileTransfers │ │ ├── LegacyGetFilesRequest.cs │ │ └── GetFileTransfersRequest.cs │ ├── ConfigureResource │ │ └── ConfigureResourceRequest.cs │ ├── GenerateReport │ │ ├── GenerateDailySummaryReportRequest.cs │ │ ├── GenerateDailySummaryReportResponse.cs │ │ └── GenerateAndDownloadDailySummaryReportResponse.cs │ ├── Middlewares │ │ └── EventBusMiddleware.cs │ ├── InitializeFileTransfer │ │ └── InitializeFileTransferRequest.cs │ ├── GetResource │ │ └── GetResourceHandler.cs │ ├── Altinn.Broker.Application.csproj │ └── DependencyInjection.cs ├── Altinn.Broker.Common │ ├── Altinn.Broker.Common.csproj │ ├── Models │ │ ├── TokenConsumer.cs │ │ └── SystemUserAuthorizationDetails.cs │ ├── Constants │ │ └── UrnConstants.cs │ └── ClaimsPrincipalExtensions.cs └── Altinn.Broker.Integrations │ ├── Altinn │ └── Events │ │ ├── Helpers │ │ └── LowerCaseNamingPolicy.cs │ │ ├── CloudEvent.cs │ │ └── ConsoleLogEventBus.cs │ ├── Azure │ ├── AzureConstants.cs │ ├── AzureStorageOptions.cs │ ├── AzureResourceManagerOptions.cs │ └── MalwareScanConfiguration.cs │ └── Hangfire │ ├── HangfireDatabaseConnectionFactory.cs │ ├── DependencyInjection.cs │ └── HangfireAppRequestFilter.cs ├── .azure ├── applications │ ├── migration │ │ └── params.bicepparam │ └── api │ │ └── params.bicepparam ├── modules │ ├── keyvault │ │ ├── upsertSecret.bicep │ │ ├── upsertSecrets.bicep │ │ ├── create.bicep │ │ ├── addSecretsOfficerRole.bicep │ │ └── addReaderRoles.bicep │ ├── virusscan │ │ └── create.bicep │ ├── identity │ │ ├── create.bicep │ │ └── addDeploymentRoles.bicep │ ├── postgreSql │ │ └── AddAdministrationAccess.bicep │ ├── subscription │ │ └── addMonitoringReaderRole.bicep │ ├── storageAccount │ │ └── create.bicep │ ├── dbBackupStorage │ │ └── create.bicep │ ├── containerApp │ │ └── fetchEventGridIps.bicep │ ├── policy │ │ └── assignBrokerTags.bicep │ └── migrationJob │ │ └── main.bicep ├── bicepconfig.json └── infrastructure │ └── params.bicepparam ├── docs ├── TestingLargeFile.md └── LoadTesting.md ├── renovate.json ├── Dockerfile └── docker-compose.yml /version.txt: -------------------------------------------------------------------------------- 1 | 1.1.0 -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /.github/CODEOWNERS @altinn/team-messaging -------------------------------------------------------------------------------- /tests/Altinn.Broker.LoadTests/data/testfile.txt: -------------------------------------------------------------------------------- 1 | this is a testfile -------------------------------------------------------------------------------- /.bruno/Resource/folder.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Resource 3 | seq: 5 4 | } 5 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.Tests/Data/ManifestFileTests/payload.txt: -------------------------------------------------------------------------------- 1 | This is a payload -------------------------------------------------------------------------------- /tests/Altinn.Broker.Tests/Data/ManifestFileTests/Empty.zip: -------------------------------------------------------------------------------- 1 | PK -------------------------------------------------------------------------------- /.bruno/FileTransfer/folder.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: FileTransfer 3 | seq: 4 4 | } 5 | -------------------------------------------------------------------------------- /.bruno/ServiceOwner/folder.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: ServiceOwner 3 | seq: 6 4 | } 5 | -------------------------------------------------------------------------------- /.bruno/Authentication/folder.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Authentication 3 | seq: 3 4 | } 5 | -------------------------------------------------------------------------------- /.bruno/SystemRegister/folder.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: SystemRegister 3 | seq: 7 4 | } 5 | -------------------------------------------------------------------------------- /.bruno/bruno.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1", 3 | "name": "Altinn.Broker", 4 | "type": "collection" 5 | } 6 | -------------------------------------------------------------------------------- /start.ps1: -------------------------------------------------------------------------------- 1 | docker compose up -d 2 | dotnet watch --project ./src/Altinn.Broker.API/Altinn.Broker.API.csproj 3 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Migrations/V0004__add_hangfire_job_id.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE broker.file_transfer 2 | ADD hangfire_job_id character varying(100) NULL; 3 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Enums/RoleExt.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Enums; 2 | 3 | public enum RoleExt 4 | { 5 | Recipient, 6 | Sender 7 | } 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/Enums/StorageProviderType.cs: -------------------------------------------------------------------------------- 1 | public enum StorageProviderType 2 | { 3 | Altinn3Azure, 4 | Altinn3AzureWithoutVirusScan 5 | } 6 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Migrations/V0008__manifest_shim.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE broker.altinn_resource 2 | ADD use_manifest_file_shim bool NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.Tests/Data/ManifestFileTests/Manifest.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/altinn/altinn-broker/main/tests/Altinn.Broker.Tests/Data/ManifestFileTests/Manifest.xml -------------------------------------------------------------------------------- /tests/Altinn.Broker.Tests/Data/ManifestFileTests/Payload.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/altinn/altinn-broker/main/tests/Altinn.Broker.Tests/Data/ManifestFileTests/Payload.zip -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/Enums/ResourceAccessLevel.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Domain.Enums; 2 | public enum ResourceAccessLevel 3 | { 4 | Read, 5 | Write 6 | } 7 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Services/Enums/AltinnEventSubjectRole.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Services.Enums; 2 | 3 | public enum AltinnEventSubjectRole 4 | { 5 | Sender, 6 | Recipient 7 | } 8 | 9 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.UseCaseTests/fixtures/usecase-broker-test-file.txt: -------------------------------------------------------------------------------- 1 | This 2 | Is 3 | A 4 | Test 5 | File 6 | For 7 | Use 8 | Case 9 | Tests 10 | In 11 | The 12 | Broker 13 | Repository 14 | ! 15 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Options/ReportStorageOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Options; 2 | 3 | public class ReportStorageOptions 4 | { 5 | public required string ConnectionString { get; set; } 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Options/DatabaseOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Persistence.Options; 2 | 3 | public class DatabaseOptions 4 | { 5 | public required string ConnectionString { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.Tests/Data/ManifestFileTests/PayloadWithExistingManifest.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/altinn/altinn-broker/main/tests/Altinn.Broker.Tests/Data/ManifestFileTests/PayloadWithExistingManifest.zip -------------------------------------------------------------------------------- /tests/Altinn.Broker.UseCaseTests/fixtures/usecase-broker-test-file2.txt: -------------------------------------------------------------------------------- 1 | This 2 | Is 3 | A 4 | Test 5 | 2 6 | File 7 | For 8 | Use 9 | Case 10 | Tests 11 | In 12 | The 13 | Broker 14 | Repository 15 | ! 16 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Models/Maskinporten/MaskinportenSecurityTokenException.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.API.Models.Maskinporten; 2 | 3 | public sealed class MaskinportenSecurityTokenException : Exception 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/Enums/SearchRole.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Domain.Enums; 2 | 3 | public enum SearchRole 4 | { 5 | Both = 0, 6 | Recipient = 1, 7 | Sender = 2 8 | } 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.LoadTests/docker-compose-loadtest.yml: -------------------------------------------------------------------------------- 1 | services: 2 | k6-test: 3 | image: grafana/k6:latest 4 | command: run /test.js 5 | volumes: 6 | - ./test.js:/test.js 7 | - ./data:/data 8 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Enums/LegacyRecipientFileStatusExt.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Enums; 2 | 3 | public enum LegacyRecipientFileStatusExt 4 | { 5 | Initialized, 6 | DownloadStarted, 7 | DownloadConfirmed 8 | } 9 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Migrations/V0013__optimize_legacy_search3.sql: -------------------------------------------------------------------------------- 1 | drop index concurrently "broker"."actor_file_transfer_status_file_transfer_id_fk_idx"; 2 | drop index concurrently "broker"."ix_file_transfer_status_id"; -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Enums/RecipientFileTransferStatusExt.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Enums; 2 | 3 | public enum RecipientFileTransferStatusExt 4 | { 5 | Initialized, 6 | DownloadStarted, 7 | DownloadConfirmed 8 | } 9 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/ActorEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Domain; 2 | 3 | public class ActorEntity 4 | { 5 | public long ActorId { get; set; } 6 | public required string ActorExternalId { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Migrations/V0003__idempotency_event.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE broker.idempotency_event ( 2 | idempotency_event_id_pk character varying(80) PRIMARY KEY, 3 | created timestamp without time zone NOT NULL 4 | ); -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/Enums/ActorFileStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Domain.Enums; 2 | 3 | public enum ActorFileTransferStatus 4 | { 5 | Initialized = 0, 6 | DownloadStarted = 1, 7 | DownloadConfirmed = 2 8 | } 9 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/CleanupUseCaseTests/CleanupUseCaseTestsRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Application.CleanupUseCaseTests; 2 | public class CleanupUseCaseTestsRequest 3 | { 4 | public required string TestTag { get; set; } 5 | } -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Helpers/SanitizerMethods.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Helpers; 2 | public static class SanitizerMethods 3 | { 4 | public static string SanitizeForLogs(this string input) => input.Replace(Environment.NewLine, null); 5 | } 6 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Configuration/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.API.Configuration; 2 | 3 | public static class Constants 4 | { 5 | public const string OrgNumberPattern = @"^(?:\d{4}:\d{9}|urn:altinn:organization:identifier-no:\d{9})$"; 6 | } 7 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/GetFileTransferDetails/GetFileTransferDetailsRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Application.GetFileTransferDetails; 2 | 3 | public class GetFileTransferDetailsRequest 4 | { 5 | public Guid FileTransferId { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Services/IAltinnRegisterService.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Services; 2 | public interface IAltinnRegisterService 3 | { 4 | Task LookUpOrganizationId(string organizationId, CancellationToken cancellationToken); 5 | } 6 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.UseCaseTests/helpers/commonUtils.js: -------------------------------------------------------------------------------- 1 | export function toUrn(id) { 2 | if (!id) return ''; 3 | if (id.includes(':')) return id; 4 | if (/^\d{9}$/.test(id)) return `urn:altinn:organization:identifier-no:${id}`; 5 | return id; 6 | } -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Migrations/V0006__add_party_id.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE broker.party ( 2 | organization_number_pk character varying(14) PRIMARY KEY, 3 | party_id character varying(20) NOT NULL, 4 | created timestamp without time zone NOT NULL 5 | ); -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Enums/DeploymentStatusExt.cs: -------------------------------------------------------------------------------- 1 | /// 2 | /// The status of deployment of Azure resources for storage providers 3 | /// 4 | public enum DeploymentStatusExt 5 | { 6 | NotStarted, 7 | DeployingResources, 8 | Ready 9 | } 10 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/DownloadFile/DownloadFileResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Application.DownloadFile; 2 | public class DownloadFileResponse 3 | { 4 | public required string FileName { get; set; } 5 | public required Stream DownloadStream { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.Tests/Data/postgresql.conf: -------------------------------------------------------------------------------- 1 | # This file will override the default PostgreSQL configuration 2 | max_prepared_transactions = 64 3 | 4 | # Include all other settings from the default postgresql.conf 5 | include_if_exists = '/usr/share/postgresql/postgresql.conf.sample' -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Options/GeneralSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Options; 2 | 3 | public class GeneralSettings 4 | { 5 | public string SlackUrl { get; set; } = string.Empty; 6 | public string ApplicationInsightsConnectionString { get; set; } = string.Empty; 7 | } 8 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Migrations/V0007__add_graceful_ttl.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE broker.altinn_resource 2 | ADD purge_file_transfer_grace_period interval NULL; 3 | 4 | ALTER TABLE broker.altinn_resource 5 | ADD purge_file_transfer_after_all_recipients_confirmed bool NOT NULL DEFAULT true; -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Helpers/IManifestDownloadStream.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain; 2 | 3 | namespace Altinn.Broker.Core.Helpers; 4 | internal interface IManifestDownloadStream 5 | { 6 | Task AddManifestFile(FileTransferEntity fileTransferEntity, ResourceEntity resource); 7 | } 8 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/PartyEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Domain; 2 | 3 | public class PartyEntity 4 | { 5 | public DateTimeOffset Created { get; set; } 6 | public required string OrganizationNumber { get; set; } 7 | public required string PartyId { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Common/Altinn.Broker.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/DownloadFile/DownloadFileRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Application.DownloadFile; 2 | public class DownloadFileRequest 3 | { 4 | public Guid FileTransferId { get; set; } 5 | public bool IsLegacy { get; set; } 6 | public string? OnBehalfOfConsumer { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Enums/LegacyFileStatusExt.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Enums; 2 | 3 | public enum LegacyFileStatusExt 4 | { 5 | Initialized, 6 | UploadStarted, 7 | UploadProcessing, 8 | Published, 9 | Cancelled, 10 | AllConfirmedDownloaded, 11 | Purged, 12 | Failed 13 | } 14 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Migrations/V0012__optimize_legacy_search2.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX file_transfer_created_idx ON broker.file_transfer (created); 2 | CREATE INDEX resource_created_idx ON broker.altinn_resource (created); 3 | CREATE INDEX file_transfer_sender_actor_id_fk_idx ON broker.file_transfer(sender_actor_id_fk) -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Enums/FileTransferStatusExt.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Enums; 2 | 3 | public enum FileTransferStatusExt 4 | { 5 | Initialized, 6 | UploadStarted, 7 | UploadProcessing, 8 | Published, 9 | Cancelled, 10 | AllConfirmedDownloaded, 11 | Purged, 12 | Failed 13 | } 14 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/ConfirmDownload/ConfirmDownloadRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Application.ConfirmDownload; 2 | public class ConfirmDownloadRequest 3 | { 4 | public Guid FileTransferId { get; set; } 5 | public bool IsLegacy { get; set; } 6 | public string? OnBehalfOfConsumer { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /.bruno/FileTransfer/{fileTransferId}/Overview.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Overview 3 | type: http 4 | seq: 1 5 | } 6 | 7 | get { 8 | url: {{broker_base_url}}/broker/api/v1/filetransfer/{{fileTransferId}} 9 | body: none 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: {{recipient_token}} 15 | } 16 | -------------------------------------------------------------------------------- /.bruno/FileTransfer/{fileTransferId}/Download.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Download 3 | type: http 4 | seq: 4 5 | } 6 | 7 | get { 8 | url: {{broker_base_url}}/broker/api/v1/filetransfer/{{fileTransferId}}/download 9 | body: none 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: {{recipient_token}} 15 | } 16 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/GetFileTransferOverview/GetFileTransferOverviewsResponse.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain; 2 | 3 | namespace Altinn.Broker.Application.GetFileTransferOverview; 4 | 5 | public class GetFileTransferOverviewsResponse 6 | { 7 | public required IReadOnlyList FileTransfers { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Options/AltinnOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Options; 2 | 3 | public class AltinnOptions 4 | { 5 | public string OpenIdWellKnown { get; set; } = string.Empty; 6 | public string PlatformGatewayUrl { get; set; } = string.Empty; 7 | public string PlatformSubscriptionKey { get; set; } = string.Empty; 8 | } 9 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Migrations/V0017__remove_property_length_limit.sql: -------------------------------------------------------------------------------- 1 | -- Remove DB-level length limits for file transfer properties. 2 | 3 | ALTER TABLE broker.file_transfer_property 4 | ALTER COLUMN key TYPE character varying; 5 | 6 | ALTER TABLE broker.file_transfer_property 7 | ALTER COLUMN value TYPE character varying; 8 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/Enums/DeploymentStatus.cs: -------------------------------------------------------------------------------- 1 | 2 | /// 3 | /// In the context of Azure deployment, "Prepared" corresponds to resource group deployed and "Ready" corresponds to all resources ready 4 | /// 5 | public enum DeploymentStatus 6 | { 7 | NotStarted, 8 | DeployingResources, 9 | Ready 10 | } 11 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/FileTransferStatusEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Domain; 2 | public class FileTransferStatusEntity 3 | { 4 | public Guid FileTransferId { get; set; } 5 | public Enums.FileTransferStatus Status { get; set; } 6 | public DateTimeOffset Date { get; set; } 7 | public string? DetailedStatus { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Repositories/IIdempotencyEventRepository.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Repositories; 2 | 3 | public interface IIdempotencyEventRepository 4 | { 5 | Task AddIdempotencyEventAsync(string id, CancellationToken cancellationToken); 6 | Task TryAddIdempotencyEventAsync(string id, CancellationToken cancellationToken); 7 | } 8 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Repositories/IPartyRepository.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain; 2 | 3 | namespace Altinn.Broker.Core.Repositories; 4 | public interface IPartyRepository 5 | { 6 | Task GetParty(string organizationId, CancellationToken cancellationToken); 7 | Task InitializeParty(string organizationId, string partyId); 8 | } 9 | -------------------------------------------------------------------------------- /.azure/applications/migration/params.bicepparam: -------------------------------------------------------------------------------- 1 | using './main.bicep' 2 | 3 | param location = 'norwayeast' 4 | param keyVaultName = readEnvironmentVariable('KEY_VAULT_NAME') 5 | param keyVaultUrl = readEnvironmentVariable('KEY_VAULT_URL') 6 | param namePrefix = readEnvironmentVariable('NAME_PREFIX') 7 | param appVersion = readEnvironmentVariable('APP_VERSION') 8 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Common/Models/TokenConsumer.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Altinn.Broker.Common.Helpers.Models; 4 | public class TokenConsumer 5 | { 6 | [JsonPropertyName("authority")] 7 | public string Authority { get; set; } 8 | 9 | [JsonPropertyName("ID")] 10 | public string ID { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/Enums/FileTransferStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Domain.Enums; 2 | 3 | public enum FileTransferStatus 4 | { 5 | Initialized = 0, 6 | UploadStarted = 1, 7 | UploadProcessing = 2, 8 | Published = 3, 9 | Cancelled = 4, 10 | AllConfirmedDownloaded = 5, 11 | Purged = 6, 12 | Failed = 7 13 | } 14 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/Webhooks/MalwareScan/ScanResultDetails.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Domain; 2 | 3 | public class ScanResultDetails 4 | { 5 | public List MalwareNamesFound { get; set; } = new List(); 6 | public string Sha256 { get; set; } = string.Empty; 7 | public string NotScannedReason { get; set; } = string.Empty; 8 | } 9 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Integrations/Altinn/Events/Helpers/LowerCaseNamingPolicy.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Text.Json; 3 | 4 | namespace Altinn.Broker.Integrations.Altinn.Events.Helpers; 5 | internal class LowerCaseNamingPolicy : JsonNamingPolicy 6 | { 7 | public override string ConvertName(string name) 8 | { 9 | return name.ToLower(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.bruno/FileTransfer/Search.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Search 3 | type: http 4 | seq: 2 5 | } 6 | 7 | get { 8 | url: {{broker_base_url}}/broker/api/v1/filetransfer?resourceId={{resource_id}} 9 | body: none 10 | auth: bearer 11 | } 12 | 13 | params:query { 14 | resourceId: {{resource_id}} 15 | } 16 | 17 | auth:bearer { 18 | token: {{recipient_token}} 19 | } 20 | -------------------------------------------------------------------------------- /.bruno/SystemRegister/System Overview.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: System Overview 3 | type: http 4 | seq: 4 5 | } 6 | 7 | get { 8 | url: {{platform_base_url}}/authentication/api/v1/systemuser/vendor/bysystem/{{serviceowner_orgnumber}}_{{resource_id}}_broker 9 | body: json 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: {{systemprovider_token}} 15 | } 16 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Services/Enums/AltinnEventType.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Services.Enums; 2 | 3 | public enum AltinnEventType 4 | { 5 | FileTransferInitialized, 6 | UploadProcessing, 7 | Published, 8 | UploadFailed, 9 | DownloadConfirmed, 10 | AllConfirmedDownloaded, 11 | FilePurged, 12 | FileNeverConfirmedDownloaded 13 | } 14 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/ActorFileTransferStatusEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Domain; 2 | 3 | public class ActorFileTransferStatusEntity 4 | { 5 | public Guid FileTransferId { get; set; } 6 | public required ActorEntity Actor { get; set; } 7 | public Enums.ActorFileTransferStatus Status { get; set; } 8 | public DateTimeOffset Date { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Repositories/IActorRepository.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain; 2 | 3 | namespace Altinn.Broker.Core.Repositories; 4 | 5 | public interface IActorRepository 6 | { 7 | Task AddActorAsync(ActorEntity actor, CancellationToken cancellationToken); 8 | Task GetActorAsync(string actorReference, CancellationToken cancellationToken); 9 | } 10 | -------------------------------------------------------------------------------- /.azure/modules/keyvault/upsertSecret.bicep: -------------------------------------------------------------------------------- 1 | param destKeyVaultName string 2 | param secretName string 3 | @secure() 4 | param secretValue string 5 | 6 | resource secret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = { 7 | name: '${destKeyVaultName}/${secretName}' 8 | properties: { 9 | value: secretValue 10 | } 11 | } 12 | 13 | output secretUri string = secret.properties.secretUri 14 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Integrations/Azure/AzureConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Integrations.Azure; 2 | public static class AzureConstants 3 | { 4 | public const string AzuriteUrl = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"; 5 | } 6 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Models/FileTransferUploadResponseExt.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.API.Models; 2 | 3 | /// 4 | /// Represents the response from uploading a file transfer. 5 | /// 6 | public class FileTransferUploadResponseExt 7 | { 8 | /// 9 | /// The ID of the file transfer. 10 | /// 11 | public Guid FileTransferId { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Models/FileTransferInitializeResponseExt.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.API.Models; 2 | 3 | /// 4 | /// Represents the response from initializing a file transfer. 5 | /// 6 | public class FileTransferInitializeResponseExt 7 | { 8 | /// 9 | /// The ID of the file transfer. 10 | /// 11 | public Guid FileTransferId { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Models/ServiceOwner/ServiceOwnerInitializeExt.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Models.ServiceOwner; 2 | 3 | /// 4 | /// Represents the Broker properties of a service owner. 5 | /// 6 | public class ServiceOwnerInitializeExt 7 | { 8 | /// 9 | /// The name of the service owner. 10 | /// 11 | public required string Name { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/UploadFile/UploadFileRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Application.UploadFile; 2 | 3 | public class UploadFileRequest 4 | { 5 | public Guid FileTransferId { get; set; } 6 | public required Stream UploadStream { get; set; } 7 | public bool IsLegacy { get; set; } 8 | public long ContentLength { get; set; } 9 | public string? OnBehalfOfConsumer { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/ApplicationConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Application.Settings; 2 | 3 | public static class ApplicationConstants 4 | { 5 | public const long MaxFileUploadSize = 32L * 50000 * 1024 * 1024; 6 | public const long MaxVirusScanUploadSize = 2L * 1024 * 1024 * 1024; 7 | public const string DefaultGracePeriod = "PT2H"; 8 | public const string MaxGracePeriod = "PT24H"; 9 | } 10 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/GetFileTransferOverview/GetFileTransferOverviewResponse.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain; 2 | 3 | namespace Altinn.Broker.Application.GetFileTransferOverview; 4 | 5 | public class GetFileTransferOverviewResponse 6 | { 7 | public required FileTransferEntity FileTransfer { get; set; } 8 | public required List FileTransferEvents { get; set; } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/StorageProviderEntity.cs: -------------------------------------------------------------------------------- 1 | public class StorageProviderEntity 2 | { 3 | public long Id { get; set; } 4 | public DateTimeOffset Created { get; set; } 5 | 6 | public StorageProviderType Type { get; set; } 7 | 8 | public required string ResourceName { get; set; } 9 | public required string ServiceOwnerId { get; set; } 10 | public required bool Active { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/CleanupUseCaseTests/CleanupUseCaseTestsResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Application.CleanupUseCaseTests; 2 | 3 | public class CleanupUseCaseTestsResponse 4 | { 5 | public required string ResourceId { get; set; } 6 | public required string TestTag { get; set; } 7 | public required int FileTransfersFound { get; set; } 8 | public required string DeleteFileTransfersJobId { get; set; } 9 | } -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/PurgeFileTransfer/PurgeFileTransferRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Application.PurgeFileTransfer; 2 | 3 | public enum PurgeTrigger 4 | { 5 | FileTransferExpiry, 6 | AllConfirmedDownloaded, 7 | MalwareScanFailed 8 | } 9 | 10 | public class PurgeFileTransferRequest 11 | { 12 | public Guid FileTransferId { get; set; } 13 | public PurgeTrigger PurgeTrigger { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Migrations/V0019__cascade_all_tables.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE broker.actor_file_transfer_status 2 | DROP CONSTRAINT actor_file_transfer_status_file_transfer_id_fk_fkey; 3 | 4 | ALTER TABLE broker.actor_file_transfer_status 5 | ADD CONSTRAINT actor_file_transfer_status_file_transfer_id_fk_fkey 6 | FOREIGN KEY (file_transfer_id_fk) 7 | REFERENCES broker.file_transfer(file_transfer_id_pk) 8 | ON DELETE CASCADE; -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Repositories/IServiceOwnerRepository.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain; 2 | 3 | namespace Altinn.Broker.Core.Repositories; 4 | public interface IServiceOwnerRepository 5 | { 6 | Task InitializeServiceOwner(string sub, string name); 7 | Task GetServiceOwner(string sub); 8 | Task InitializeStorageProvider(string sub, string resourceName, StorageProviderType storageType); 9 | } 10 | -------------------------------------------------------------------------------- /.bruno/Authentication/Exchange sender token for Altinn token.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Exchange sender token for Altinn token 3 | type: http 4 | seq: 4 5 | } 6 | 7 | get { 8 | url: {{platform_base_url}}/authentication/api/v1/exchange/maskinporten 9 | body: none 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: {{sender_maskinporten_token}} 15 | } 16 | 17 | script:post-response { 18 | bru.setVar("sender_token", res.body); 19 | } 20 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Migrations/V0009__add_external_service_code.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE broker.altinn_resource 2 | ADD external_service_code_legacy varchar(7); 3 | COMMENT ON COLUMN broker.altinn_resource.external_service_code_legacy IS 'Part of legacy solution'; 4 | 5 | ALTER TABLE broker.altinn_resource 6 | ADD external_service_edition_code_legacy int; 7 | COMMENT ON COLUMN broker.altinn_resource.external_service_edition_code_legacy IS 'Part of legacy solution'; -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/GetFileTransferOverview/GetFileTransferOverviewRequest.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain; 2 | 3 | namespace Altinn.Broker.Application.GetFileTransferOverview; 4 | 5 | public class GetFileTransferOverviewRequest 6 | { 7 | public List? FileTransferIds { get; set; } 8 | public Guid FileTransferId { get; set; } 9 | public bool IsLegacy { get; set; } 10 | public string? OnBehalfOfConsumer { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /.bruno/Authentication/Exchange recipient token for Altinn token.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Exchange recipient token for Altinn token 3 | type: http 4 | seq: 6 5 | } 6 | 7 | get { 8 | url: {{platform_base_url}}/authentication/api/v1/exchange/maskinporten 9 | body: none 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: {{recipient_maskinporten_token}} 15 | } 16 | 17 | script:post-response { 18 | bru.setVar("recipient_token", res.body); 19 | } 20 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Models/Maskinporten/MaskinportenConsumer.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Altinn.Broker.Models.Maskinporten; 4 | 5 | [method: JsonConstructor] 6 | public class MaskinportenConsumer( 7 | string authority, 8 | string id 9 | ) 10 | { 11 | [JsonPropertyName("authority")] 12 | public string Authority { get; } = authority; 13 | 14 | [JsonPropertyName("ID")] 15 | public string ID { get; } = id; 16 | } 17 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Models/Maskinporten/MaskinportenSupplier.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Altinn.Broker.Models.Maskinporten; 4 | 5 | [method: JsonConstructor] 6 | public class MaskinportenSupplier( 7 | string authority, 8 | string id 9 | ) 10 | { 11 | [JsonPropertyName("authority")] 12 | public string Authority { get; } = authority; 13 | 14 | [JsonPropertyName("ID")] 15 | public string ID { get; } = id; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /.bruno/Authentication/Exchange systemprovider token for Altinn token.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Exchange systemprovider token for Altinn token 3 | type: http 4 | seq: 2 5 | } 6 | 7 | get { 8 | url: {{platform_base_url}}/authentication/api/v1/exchange/maskinporten 9 | body: none 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: {{systemprovider_maskinporten_token}} 15 | } 16 | 17 | script:post-response { 18 | bru.setVar("systemprovider_token", res.body); 19 | } 20 | -------------------------------------------------------------------------------- /.bruno/FileTransfer/{fileTransferId}/Upload.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Upload 3 | type: http 4 | seq: 3 5 | } 6 | 7 | post { 8 | url: {{broker_base_url}}/broker/api/v1/filetransfer/{{fileTransferId}}/upload 9 | body: file 10 | auth: bearer 11 | } 12 | 13 | headers { 14 | Content-Type: application/octet-stream 15 | } 16 | 17 | auth:bearer { 18 | token: {{sender_token}} 19 | } 20 | 21 | body:file { 22 | file: @file(collection.bru) @contentType(application/octet-stream) 23 | } 24 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/GetFileTransferDetails/GetFileTransferDetailsResponse.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain; 2 | 3 | namespace Altinn.Broker.Application.GetFileTransferDetails; 4 | 5 | public class GetFileTransferDetailsResponse 6 | { 7 | public required List ActorEvents { get; set; } 8 | public required List FileTransferEvents { get; set; } 9 | public required FileTransferEntity FileTransfer { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/Enums/AltinnVersion.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Domain.Enums; 2 | 3 | /// 4 | /// Specifies the Altinn version for a file transfer 5 | /// 6 | public enum AltinnVersion 7 | { 8 | /// 9 | /// File transfer from Altinn 2 (legacy system) 10 | /// 11 | Altinn2 = 0, 12 | 13 | /// 14 | /// File transfer from Altinn 3 (current system) 15 | /// 16 | Altinn3 = 1 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Services/IEventBus.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Services.Enums; 2 | 3 | namespace Altinn.Broker.Core.Services; 4 | 5 | public interface IEventBus 6 | { 7 | Task Publish( 8 | AltinnEventType type, 9 | string resourceId, 10 | string fileTransferId, 11 | string? organizationId = null, 12 | Guid? guid = null, 13 | AltinnEventSubjectRole? subjectRole = null, 14 | CancellationToken cancellationToken = default); 15 | } 16 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Models/Recipient/LegacyRecipientFileStatusEventExt.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Enums; 2 | 3 | namespace Altinn.Broker.Models; 4 | 5 | public class LegacyRecipientFileStatusEventExt 6 | { 7 | public string Recipient { get; set; } = string.Empty; 8 | public LegacyRecipientFileStatusExt RecipientFileStatusCode { get; set; } 9 | public string RecipientFileStatusText { get; set; } = string.Empty; 10 | public DateTimeOffset RecipientFileStatusChanged { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Options/SlackSettings.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | 3 | namespace Altinn.Broker.Core.Options; 4 | 5 | public class SlackSettings 6 | { 7 | private readonly IHostEnvironment _hostEnvironment; 8 | 9 | public SlackSettings(IHostEnvironment hostEnvironment) 10 | { 11 | _hostEnvironment = hostEnvironment; 12 | } 13 | 14 | public string NotificationChannel => _hostEnvironment.IsProduction() ? "#mf-varsling-critical" : "#test-varslinger"; 15 | } -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/Webhooks/MalwareScan/ScanResultData.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Domain; 2 | 3 | public class ScanResultData 4 | { 5 | public Guid CorrelationId { get; set; } 6 | public string BlobUri { get; set; } = string.Empty; 7 | public string ETag { get; set; } = string.Empty; 8 | public DateTime ScanFinishedTimeUtc { get; set; } 9 | public string ScanResultType { get; set; } = string.Empty; 10 | public ScanResultDetails? ScanResultDetails { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Integrations/Hangfire/HangfireDatabaseConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using Hangfire.PostgreSql; 2 | 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | using Npgsql; 6 | 7 | namespace Altinn.Broker.Integrations.Hangfire; 8 | 9 | public class HangfireDatabaseConnectionFactory(IServiceProvider serviceProvider) : IConnectionFactory 10 | { 11 | public NpgsqlConnection GetOrCreateConnection() => 12 | serviceProvider.GetRequiredService().CreateConnection(); 13 | } 14 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Migrations/V0005__add_resource_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE broker.altinn_resource ( 2 | resource_id_pk character varying(80) PRIMARY KEY, 3 | created timestamp without time zone NOT NULL, 4 | max_file_transfer_size bigint, 5 | file_transfer_time_to_live interval, 6 | organization_number character varying(14) NOT NULL, 7 | service_owner_id_fk character varying(14) NOT NULL, 8 | FOREIGN KEY (service_owner_id_fk) REFERENCES broker.service_owner (service_owner_id_pk) 9 | ); -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Helpers/SecurityHeadersMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Primitives; 2 | 3 | namespace Altinn.Broker.Helpers; 4 | public class SecurityHeadersMiddleware(RequestDelegate next) 5 | { 6 | public async Task InvokeAsync(HttpContext context) 7 | { 8 | context.Response.Headers.Append("X-Content-Type-Options", new StringValues("nosniff")); 9 | context.Response.Headers.Append("Cache-Control", new StringValues("no-store")); 10 | 11 | await next(context); 12 | } 13 | } -------------------------------------------------------------------------------- /.azure/modules/virusscan/create.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | resource StorageAccounts 'Microsoft.Security/pricings@2024-01-01' = { 4 | name: 'StorageAccounts' 5 | properties: { 6 | pricingTier: 'Standard' 7 | 8 | subPlan: 'DefenderForStorageV2' 9 | extensions: [ 10 | { 11 | name: 'OnUploadMalwareScanning' 12 | isEnabled: 'False' 13 | } 14 | { 15 | name: 'SensitiveDataDiscovery' 16 | isEnabled: 'False' 17 | } 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Models/Recipient/LegacyRecipientFileStatusDetailsExt.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Enums; 2 | 3 | namespace Altinn.Broker.Models; 4 | 5 | public class LegacyRecipientFileStatusDetailsExt 6 | { 7 | public string Recipient { get; set; } = string.Empty; 8 | public LegacyRecipientFileStatusExt CurrentRecipientFileStatusCode { get; set; } 9 | public string CurrentRecipientFileStatusText { get; set; } = string.Empty; 10 | public DateTimeOffset CurrentRecipientFileStatusChanged { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /docs/TestingLargeFile.md: -------------------------------------------------------------------------------- 1 | # Testing large file 2 | 3 | In order to test upload of large files you can use the /tests/Altinn.Broker.Tests.LargeFile console application. Run it and follow the prompts. Use the username and password for [Altinn Test Tools](https://github.com/Altinn/AltinnTestTools). 4 | 5 | If you want to test it from another environment use the Dockerfile to deploy it as a container somewhere (like as an Azure Container App Job) and set the correct environment variables (find the UPPERCASE_SNAKE_CASE variables in Program.cs). 6 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "labels": [ 7 | "kind/dependencies" 8 | ], 9 | "schedule": [ 10 | "before 7am on Monday", 11 | "before 7am on Friday" 12 | ], 13 | "packageRules": [ 14 | { 15 | "matchPaths": [ "**/*.bicep" ], 16 | "enabled": false 17 | }, 18 | { 19 | "groupName": "All dependencies", 20 | "matchUpdateTypes": [ "major", "minor", "patch" ] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Migrations/V0016__activate_cron.sql: -------------------------------------------------------------------------------- 1 | DO $do$ 2 | BEGIN 3 | -- Try to create the extension 4 | CREATE EXTENSION IF NOT EXISTS pg_cron; 5 | 6 | -- If successful, schedule the weekly ANALYZE job 7 | PERFORM cron.schedule( 8 | 'weekly_analyze', 9 | '0 4 * * 0', 10 | $$ ANALYZE; $$ 11 | ); 12 | EXCEPTION 13 | WHEN OTHERS THEN 14 | -- Log the error but don't fail the migration 15 | RAISE NOTICE 'pg_cron could not be activated: %', SQLERRM; 16 | END 17 | $do$; -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/IHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | using Altinn.Broker.Application; 4 | 5 | using OneOf; 6 | 7 | namespace Altinn.Broker.Core.Application; 8 | internal interface IHandler 9 | { 10 | Task> Process(TRequest request, ClaimsPrincipal? user, CancellationToken cancellationToken); 11 | } 12 | 13 | internal interface IHandler 14 | { 15 | Task> Process(ClaimsPrincipal? user, CancellationToken cancellationToken); 16 | } 17 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Migrations/V0002__hangfire.sql: -------------------------------------------------------------------------------- 1 | CREATE SCHEMA hangfire; 2 | 3 | DO $do$ 4 | DECLARE 5 | role_count INTEGER; 6 | BEGIN 7 | SELECT COUNT(*) INTO role_count FROM pg_roles WHERE rolname = 'azure_pg_admin'; 8 | 9 | IF role_count > 0 THEN 10 | GRANT ALL ON SCHEMA hangfire TO azure_pg_admin; 11 | ALTER DEFAULT PRIVILEGES IN SCHEMA hangfire GRANT ALL ON TABLES TO azure_pg_admin; 12 | ALTER DEFAULT PRIVILEGES IN SCHEMA hangfire GRANT ALL ON SEQUENCES TO azure_pg_admin; 13 | END IF; 14 | END 15 | $do$; 16 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.Tests/Data/WebHookSubscriptionValidationTest.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": "2d1781af-3a4c-4d7c-bd0c-e34b19da4e66", 3 | "topic": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", 4 | "subject": "", 5 | "data": { 6 | "validationCode": "512d38b6-c7b8-40c8-89fe-f46f9e9622b6", 7 | "validationUrl": "https://www.contoso.com/" 8 | }, 9 | "eventType": "Microsoft.EventGrid.SubscriptionValidationEvent", 10 | "eventTime": "2018-01-25T22:12:19.4556811Z", 11 | "metadataVersion": "1", 12 | "dataVersion": "1" 13 | }] -------------------------------------------------------------------------------- /.github/workflows/test-use-cases-PROD.yml: -------------------------------------------------------------------------------- 1 | name: Test Use Cases - PROD 2 | 3 | on: 4 | schedule: 5 | - cron: '*/15 * * * *' 6 | 7 | jobs: 8 | test-prod: 9 | name: Run use case tests on production 10 | uses: ./.github/workflows/test-use-cases.yml 11 | with: 12 | environment: use-case-production 13 | secrets: inherit 14 | 15 | test-prod-legacy: 16 | name: Run legacy use case tests on prod 17 | uses: ./.github/workflows/test-use-cases-legacy.yml 18 | with: 19 | environment: use-case-production 20 | secrets: inherit -------------------------------------------------------------------------------- /.github/workflows/test-use-cases-TT02.yml: -------------------------------------------------------------------------------- 1 | name: Test Use Cases - TT02 2 | 3 | on: 4 | schedule: 5 | - cron: '*/15 * * * *' 6 | 7 | jobs: 8 | test-staging: 9 | name: Run use case tests on staging 10 | uses: ./.github/workflows/test-use-cases.yml 11 | with: 12 | environment: use-case-staging 13 | secrets: inherit 14 | 15 | test-staging-legacy: 16 | name: Run legacy use case tests on staging 17 | uses: ./.github/workflows/test-use-cases-legacy.yml 18 | with: 19 | environment: use-case-staging 20 | secrets: inherit -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Repositories/IActorFileTransferStatusRepository.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain; 2 | 3 | namespace Altinn.Broker.Core.Repositories; 4 | public interface IActorFileTransferStatusRepository 5 | { 6 | Task InsertActorFileTransferStatus( 7 | Guid fileTransferId, 8 | Domain.Enums.ActorFileTransferStatus status, 9 | string actorExternalReference, 10 | CancellationToken cancellationToken 11 | ); 12 | Task> GetActorEvents(Guid fileTransferId, CancellationToken cancellationToken); 13 | } 14 | -------------------------------------------------------------------------------- /.azure/modules/identity/create.bicep: -------------------------------------------------------------------------------- 1 | @secure() 2 | param namePrefix string 3 | param location string 4 | 5 | resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 6 | name: '${namePrefix}-app-identity' 7 | location: location 8 | } 9 | output id string = userAssignedIdentity.id 10 | output clientId string = userAssignedIdentity.properties.clientId 11 | output principalId string = userAssignedIdentity.properties.principalId 12 | output tenantId string = userAssignedIdentity.properties.tenantId 13 | output name string = userAssignedIdentity.name 14 | -------------------------------------------------------------------------------- /.bruno/FileTransfer/{fileTransferId}/Confirm Download.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Confirm download 3 | type: http 4 | seq: 5 5 | } 6 | 7 | post { 8 | url: {{broker_base_url}}/broker/api/v1/filetransfer/{{fileTransferId}}/confirmdownload 9 | body: none 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: {{recipient_altinn_token}} 15 | } 16 | 17 | tests { 18 | test("Status code is 200", function() { 19 | expect(res.getStatus()).to.equal(200); 20 | }); 21 | 22 | test("Response is empty", function() { 23 | expect(res.getBody()).to.equal(''); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /.azure/modules/postgreSql/AddAdministrationAccess.bicep: -------------------------------------------------------------------------------- 1 | param principalId string 2 | param tenantId string 3 | param appName string 4 | param namePrefix string 5 | 6 | resource databaseServer 'Microsoft.DBforPostgreSQL/flexibleServers@2023-12-01-preview' existing = { 7 | name: '${namePrefix}-dbserver' 8 | } 9 | resource databaseAccess 'Microsoft.DBforPostgreSQL/flexibleServers/administrators@2022-12-01' = { 10 | name: principalId 11 | parent: databaseServer 12 | properties: { 13 | principalType: 'ServicePrincipal' 14 | tenantId: tenantId 15 | principalName: appName 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/validate-formatting.yml: -------------------------------------------------------------------------------- 1 | name: Validate formatting 2 | 3 | on: 4 | workflow_call: 5 | workflow_dispatch: 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | check-formatting: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Setup .NET 16 | uses: actions/setup-dotnet@v4 17 | with: 18 | dotnet-version: | 19 | 9.0.x 20 | 21 | - name: Format 22 | run: | 23 | dotnet format --verify-no-changes --verbosity diagnostic --include ./src/** 24 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Models/FileTransferInitializeAndUploadExt.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Models; 2 | 3 | 4 | /// 5 | /// A model representing the initialization and upload of a file transfer. 6 | /// 7 | public class FileTransferInitializeAndUploadExt 8 | { 9 | /// 10 | /// The metadata for the file transfer. 11 | /// 12 | public required FileTransferInitalizeExt Metadata { get; set; } 13 | 14 | /// 15 | /// The file to be uploaded. 16 | /// 17 | public required IFormFile FileTransfer { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /.azure/modules/keyvault/upsertSecrets.bicep: -------------------------------------------------------------------------------- 1 | param sourceKeyvaultName string 2 | param secrets { name: string, value: string }[] 3 | 4 | var baseName = 'secret${uniqueString(resourceGroup().id)}' 5 | 6 | module keyvaultSecret './upsertSecret.bicep' = [for i in range(0, length(secrets)): { 7 | name: '${i}deploy${baseName}' 8 | params: { 9 | destKeyVaultName: sourceKeyvaultName 10 | secretName: secrets[i].name 11 | secretValue: secrets[i].value 12 | } 13 | }] 14 | 15 | output keyvaultUris array = [for i in range(0, length(secrets)): { 16 | endpoint: keyvaultSecret[i].outputs.secretUri 17 | }] 18 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Migrations/V0015__new_legacy_search.sql: -------------------------------------------------------------------------------- 1 | -- For actor file transfer status 2 | CREATE index idx_actor_file_transfer_status_window 3 | ON broker.actor_file_transfer_status (file_transfer_id_fk, actor_id_fk, actor_file_transfer_status_id_pk DESC); 4 | 5 | -- For file transfer status 6 | CREATE INDEX idx_file_transfer_status_window 7 | ON broker.file_transfer_status (file_transfer_id_fk, file_transfer_status_id_pk DESC); 8 | 9 | -- For main query filtering 10 | CREATE INDEX idx_file_transfer_search 11 | ON broker.file_transfer (created, resource_id) 12 | INCLUDE (file_transfer_id_pk); 13 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Integrations/Altinn/Events/CloudEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Integrations.Altinn.Events; 2 | 3 | public class CloudEvent 4 | { 5 | public string SpecVersion { get; set; } = "1.0"; 6 | public Guid Id { get; set; } 7 | public string Type { get; set; } = null!; 8 | public DateTimeOffset Time { get; set; } 9 | public string Resource { get; set; } = null!; 10 | public string ResourceInstance { get; set; } = null!; 11 | public string? Subject { get; set; } 12 | public string Source { get; set; } = null!; 13 | public Dictionary? Data { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Migrations/V0014__search_indices.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX actor_file_transfer_actor_lookup_idx 2 | ON broker.actor_file_transfer_status ( 3 | file_transfer_id_fk, 4 | actor_id_fk, 5 | actor_file_transfer_status_description_id_fk DESC 6 | ); 7 | 8 | CREATE INDEX file_transfer_status_file_transfer_id_fk_status_pk_idx 9 | ON broker.file_transfer_status ( 10 | file_transfer_id_fk, 11 | file_transfer_status_id_pk DESC 12 | ); 13 | 14 | CREATE INDEX file_transfer_file_transfer_id_pk_resource_id_idx 15 | ON broker.file_transfer ( 16 | file_transfer_id_pk, 17 | resource_id 18 | ); 19 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/Webhooks/MalwareScan/EventMessage.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Domain; 2 | 3 | public class EventMessage 4 | { 5 | public string Id { get; set; } = string.Empty; 6 | public string Subject { get; set; } = string.Empty; 7 | public ScanResultData Data { get; set; } = new ScanResultData(); 8 | public string EventType { get; set; } = string.Empty; 9 | public string DataVersion { get; set; } = string.Empty; 10 | public string MetadataVersion { get; set; } = string.Empty; 11 | public DateTime EventTime { get; set; } 12 | public string Topic { get; set; } = string.Empty; 13 | } 14 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/Enums/RecipientType.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Domain.Enums; 2 | 3 | /// 4 | /// Defines the type of recipients 5 | /// 6 | public enum RecipientType : int 7 | { 8 | /// 9 | /// Specifies that the recipient is a person 10 | /// 11 | Person = 0, 12 | 13 | /// 14 | /// Specifies that the recipient is an organization 15 | /// 16 | Organization = 1, 17 | 18 | /// 19 | /// Specifies that the recipient type is unknown or could not be determined 20 | /// 21 | Unknown = 2, 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/FileTransferSearchEntity.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain.Enums; 2 | 3 | namespace Altinn.Broker.Core.Domain; 4 | 5 | public class FileTransferSearchEntity 6 | { 7 | public required ActorEntity Actor { get; set; } 8 | public FileTransferStatus? Status { get; set; } 9 | public ActorFileTransferStatus? RecipientStatus { get; set; } 10 | public DateTimeOffset? From { get; set; } 11 | public DateTimeOffset? To { get; set; } 12 | public required string ResourceId { get; set; } 13 | public string? OrderAscending { get; set; } 14 | public SearchRole Role { get; set; } = SearchRole.Both; 15 | } 16 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Migrations/V0001__enum_tables.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO broker.file_transfer_status_description (file_transfer_status_description_id_pk, file_transfer_status_description) 2 | VALUES 3 | (0, 'Initialized'), 4 | (1, 'UploadStarted'), 5 | (2, 'UploadProcessing'), 6 | (3, 'Published'), 7 | (4, 'Cancelled'), 8 | (5, 'AllConfirmedDownloaded'), 9 | (6, 'Purged'), 10 | (7, 'Failed'); 11 | 12 | INSERT INTO broker.actor_file_transfer_status_description (actor_file_transfer_status_description_id_pk, actor_file_transfer_status_description) 13 | VALUES 14 | (0, 'Initialized'), 15 | (1, 'DownloadStarted'), 16 | (2, 'DownloadConfirmed'); 17 | -------------------------------------------------------------------------------- /.azure/applications/api/params.bicepparam: -------------------------------------------------------------------------------- 1 | using './main.bicep' 2 | 3 | param namePrefix = readEnvironmentVariable('NAME_PREFIX') 4 | param location = 'norwayeast' 5 | param imageTag = readEnvironmentVariable('IMAGE_TAG') 6 | param platform_base_url = readEnvironmentVariable('PLATFORM_BASE_URL') 7 | param maskinporten_environment = readEnvironmentVariable('MASKINPORTEN_ENVIRONMENT') 8 | param environment = readEnvironmentVariable('ENVIRONMENT') 9 | param apimIp = readEnvironmentVariable('APIM_IP') 10 | 11 | // secrets 12 | param sourceKeyVaultName = readEnvironmentVariable('KEY_VAULT_NAME') 13 | param keyVaultUrl = readEnvironmentVariable('KEY_VAULT_URL') 14 | -------------------------------------------------------------------------------- /.azure/modules/subscription/addMonitoringReaderRole.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | param grafanaPrincipalId string 4 | 5 | var monitoringReaderRoleDefinitionId = '43d0d8ad-25c7-4714-9337-8ba259a9fe05' 6 | 7 | resource monitoringReaderRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 8 | name: guid(subscription().id, grafanaPrincipalId, monitoringReaderRoleDefinitionId) 9 | properties: { 10 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', monitoringReaderRoleDefinitionId) 11 | principalId: grafanaPrincipalId 12 | principalType: 'ServicePrincipal' 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /.bruno/ServiceOwner/Configure service owner for Broker.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Configure service owner for Broker 3 | type: http 4 | seq: 1 5 | } 6 | 7 | post { 8 | url: {{broker_base_url}}/broker/api/v1/serviceowner 9 | body: json 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: {{systemprovider_token}} 15 | } 16 | 17 | body:json { 18 | { 19 | "Name": "{{serviceowner_orgnumber}}" 20 | } 21 | } 22 | 23 | tests { 24 | test("Status code is 200", function() { 25 | expect(res.getStatus()).to.equal(200); 26 | }); 27 | 28 | test("Response is empty", function() { 29 | expect(res.getBody()).to.equal(''); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/GetFileTransfers/LegacyGetFilesRequest.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain; 2 | using Altinn.Broker.Core.Domain.Enums; 3 | 4 | namespace Altinn.Broker.Application.GetFileTransfers; 5 | 6 | public class LegacyGetFilesRequest 7 | { 8 | public string? ResourceId { get; set; } 9 | public FileTransferStatus? FileTransferStatus { get; set; } 10 | public ActorFileTransferStatus? RecipientFileTransferStatus { get; set; } 11 | public DateTimeOffset? From { get; set; } 12 | public DateTimeOffset? To { get; set; } 13 | public string[]? Recipients { get; set; } 14 | public string? OnBehalfOfConsumer { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/GetFileTransfers/GetFileTransfersRequest.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain; 2 | using Altinn.Broker.Core.Domain.Enums; 3 | 4 | namespace Altinn.Broker.Application.GetFileTransfers; 5 | 6 | public class GetFileTransfersRequest 7 | { 8 | public required string ResourceId { get; set; } 9 | public FileTransferStatus? Status { get; set; } 10 | public ActorFileTransferStatus? RecipientStatus { get; set; } 11 | public DateTimeOffset? From { get; set; } 12 | public DateTimeOffset? To { get; set; } 13 | 14 | public bool? OrderAscending { get; set; } 15 | 16 | public SearchRole Role { get; set; } = SearchRole.Both; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/ConfigureResource/ConfigureResourceRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Application.ConfigureResource; 2 | public class ConfigureResourceRequest 3 | { 4 | public required string ResourceId { get; set; } 5 | public long? MaxFileTransferSize { get; set; } 6 | public string? FileTransferTimeToLive { get; set; } 7 | public bool? PurgeFileTransferAfterAllRecipientsConfirmed { get; set; } = true; 8 | public string? PurgeFileTransferGracePeriod { get; set; } 9 | public bool? UseManifestFileShim { get; set; } 10 | public string? ExternalServiceCodeLegacy { get; set; } 11 | public int? ExternalServiceEditionCodeLegacy { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Repositories/IAltinnResourceRepoistory.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain; 2 | 3 | namespace Altinn.Broker.Core.Repositories; 4 | public interface IAltinnResourceRepository 5 | { 6 | Task GetResource(string resourceId, CancellationToken cancellationToken = default); 7 | 8 | /// 9 | /// Get the service owner name from Resource Registry for a given resource ID. 10 | /// This returns the name from HasCompetentAuthority.Name (e.g., "Digitaliseringsdirektoratet", "NAV", etc.) 11 | /// 12 | Task GetServiceOwnerNameOfResource(string resourceId, CancellationToken cancellationToken = default); 13 | } 14 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.UseCaseTests/helpers/cryptoUtils.js: -------------------------------------------------------------------------------- 1 | import encoding from 'k6/encoding'; 2 | 3 | export function pemToBinary(pem) { 4 | const base64 = (pem || '') 5 | .replace(/-----BEGIN[\s\S]*?-----/g, '') 6 | .replace(/-----END[\s\S]*?-----/g, '') 7 | .replace(/\s+/g, ''); 8 | return encoding.b64decode(base64, 'std'); 9 | } 10 | 11 | // Converts a string to bytes (Uint8Array) 12 | // Note: This assumes ASCII-only input (works for base64url JWT strings) 13 | export function stringToBytes(str) { 14 | const arr = new Uint8Array(str.length); 15 | for (let i = 0; i < str.length; i++) { 16 | arr[i] = str.charCodeAt(i); 17 | } 18 | return arr; 19 | } -------------------------------------------------------------------------------- /.azure/modules/keyvault/create.bicep: -------------------------------------------------------------------------------- 1 | param vaultName string 2 | param location string 3 | @secure() 4 | param tenant_id string 5 | @export() 6 | type Sku = { 7 | name: 'standard' 8 | family: 'A' 9 | } 10 | param sku Sku 11 | 12 | resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = { 13 | name: vaultName 14 | location: location 15 | properties: { 16 | enabledForTemplateDeployment: true 17 | enabledForDiskEncryption: true 18 | enabledForDeployment: true 19 | enableSoftDelete: true 20 | enablePurgeProtection: true 21 | sku: sku 22 | tenantId: tenant_id 23 | enableRbacAuthorization: true 24 | accessPolicies: [] 25 | } 26 | } 27 | 28 | output name string = keyVault.name 29 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/GenerateReport/GenerateDailySummaryReportRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Application.GenerateReport; 2 | 3 | /// 4 | /// Request parameters for generating daily summary report 5 | /// 6 | public class GenerateDailySummaryReportRequest 7 | { 8 | /// 9 | /// Whether to include Altinn2 file transfers in the report. 10 | /// If false, only Altinn3 file transfers will be included. 11 | /// Default is true (include both Altinn2 and Altinn3). 12 | /// Note: Broker only supports Altinn3, so this parameter is kept for API compatibility but has no effect. 13 | /// 14 | public bool Altinn2Included { get; set; } = true; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/Middlewares/EventBusMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Services; 2 | using Altinn.Broker.Core.Services.Enums; 3 | 4 | namespace Altinn.Broker.Application.Middlewares; 5 | public class EventBusMiddleware 6 | { 7 | private readonly IEventBus _eventBus; 8 | public EventBusMiddleware(IEventBus eventBus) 9 | { 10 | _eventBus = eventBus; 11 | } 12 | public async Task Publish(AltinnEventType type, string resourceId, string fileTransferId, string? subjectOrganizationNumber = null, Guid? guid = null, AltinnEventSubjectRole? subjectRole = null) 13 | { 14 | await _eventBus.Publish(type, resourceId, fileTransferId, subjectOrganizationNumber, guid, subjectRole); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.azure/modules/keyvault/addSecretsOfficerRole.bicep: -------------------------------------------------------------------------------- 1 | param keyvaultName string 2 | param principalType string = 'Group' 3 | param principalObjectId string 4 | 5 | var secretsOfficerRoleId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7') 6 | 7 | resource kv 'Microsoft.KeyVault/vaults@2023-07-01' existing = { 8 | name: keyvaultName 9 | } 10 | 11 | resource secretsOfficer 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 12 | name: guid(subscription().id, kv.id, principalObjectId, secretsOfficerRoleId) 13 | scope: kv 14 | properties: { 15 | roleDefinitionId: secretsOfficerRoleId 16 | principalId: principalObjectId 17 | principalType: principalType 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Repositories/IFileTransferStatusRepository.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain; 2 | using Altinn.Broker.Core.Domain.Enums; 3 | 4 | namespace Altinn.Broker.Core.Repositories; 5 | public interface IFileTransferStatusRepository 6 | { 7 | Task> GetFileTransferStatusHistory(Guid fileTransferId, CancellationToken cancellationToken); 8 | Task InsertFileTransferStatus(Guid fileTransferId, FileTransferStatus status, string? detailedFileTransferStatus = null, CancellationToken cancellationToken = default); 9 | Task> GetCurrentFileTransferStatusesOfStatusAndOlderThanDate(FileTransferStatus statusFilter, DateTime maxStatusAge, CancellationToken cancellationToken); 10 | } 11 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | categories: 6 | - title: Breaking Changes 🛠 7 | labels: 8 | - kind/breaking-change 9 | - title: New Features 🎉 10 | labels: 11 | - kind/feature 12 | - kind/feature-request 13 | - title: Bugfixes 🐛 14 | labels: 15 | - kind/bug 16 | - title: Other Changes 17 | labels: 18 | - kind/other 19 | - title: Dependency Upgrades 📦 20 | labels: 21 | - kind/dependencies 22 | - title: Enhancements 23 | labels: 24 | - kind/enhancement 25 | - title: Incident 26 | labels: 27 | - kind/incident 28 | - title: Uncategorized changes 29 | labels: 30 | - "*" 31 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/InitializeFileTransfer/InitializeFileTransferRequest.cs: -------------------------------------------------------------------------------- 1 | 2 | using Altinn.Broker.Core.Domain; 3 | 4 | namespace Altinn.Broker.Application.InitializeFileTransfer; 5 | public class InitializeFileTransferRequest 6 | { 7 | public required string ResourceId { get; set; } 8 | public required string FileName { get; set; } 9 | public required string SendersFileTransferReference { get; set; } 10 | public required string SenderExternalId { get; set; } 11 | public required List RecipientExternalIds { get; set; } 12 | public required Dictionary PropertyList { get; set; } 13 | public string? Checksum { get; set; } 14 | public bool IsLegacy { get; set; } 15 | public bool DisableVirusScan { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/check-label-for-pr.yml: -------------------------------------------------------------------------------- 1 | name: Checking label for PR 2 | on: 3 | pull_request: 4 | types: [opened, reopened, ready_for_review, labeled, unlabeled] 5 | jobs: 6 | label: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | pull-requests: write 10 | steps: 11 | - uses: mheap/github-action-required-labels@v5 12 | with: 13 | mode: minimum 14 | count: 1 15 | labels: | 16 | kind/breaking-change 17 | kind/feature 18 | kind/feature-request 19 | kind/bug 20 | kind/other 21 | kind/user-story 22 | kind/dependencies 23 | kind/documentation 24 | kind/enhancement 25 | kind/incident 26 | kind/chore 27 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/ServiceOwnerEntity.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | 3 | namespace Altinn.Broker.Core.Domain; 4 | 5 | public class ServiceOwnerEntity 6 | { 7 | public required string Id { get; set; } 8 | public required string Name { get; set; } 9 | public required List StorageProviders { get; set; } 10 | 11 | public StorageProviderEntity? GetStorageProvider(bool withVirusScan) 12 | { 13 | if (withVirusScan) 14 | { 15 | return StorageProviders.FirstOrDefault(sp => sp.Type == StorageProviderType.Altinn3Azure); 16 | } 17 | else 18 | { 19 | return StorageProviders.FirstOrDefault(sp => sp.Type == StorageProviderType.Altinn3AzureWithoutVirusScan); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Migrations/V0010__toggleable_virusscan.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE broker.file_transfer 2 | ADD use_virus_scan bool NOT NULL DEFAULT true; 3 | 4 | ALTER TABLE broker.altinn_resource 5 | ADD approved_for_disabled_virus_scan bool NOT NULL DEFAULT false; 6 | 7 | ALTER TABLE broker.storage_provider 8 | ADD active bool NOT NULL DEFAULT true; 9 | 10 | ALTER TABLE broker.storage_provider 11 | ADD CONSTRAINT storage_provider_owner_type_unique 12 | UNIQUE (service_owner_id_fk, storage_provider_type, active); 13 | 14 | ALTER TABLE broker.storage_provider 15 | DROP CONSTRAINT storage_provider_storage_provider_type_check; 16 | 17 | ALTER TABLE broker.storage_provider 18 | ADD CONSTRAINT storage_provider_type_check 19 | CHECK (storage_provider_type IN ('Altinn3Azure', 'Altinn3AzureWithoutVirusScan')); 20 | -------------------------------------------------------------------------------- /.bruno/SystemRegister/Create sender systemuser request.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Create sender systemuser request 3 | type: http 4 | seq: 2 5 | } 6 | 7 | post { 8 | url: {{platform_base_url}}/authentication/api/v1/systemuser/request/vendor 9 | body: json 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: {{systemprovider_token}} 15 | } 16 | 17 | body:json { 18 | { 19 | "externalReference": "dev-test-create_01", 20 | "systemId": "{{serviceowner_orgnumber}}_{{resource_id}}_broker", 21 | "partyOrgNo": "{{sender_orgnumber}}", 22 | "rights": [ 23 | { 24 | "Resource": [ 25 | { 26 | "id": "urn:altinn:resource", 27 | "value": "{{resource_id}}" 28 | } 29 | ] 30 | } 31 | ], 32 | "redirectUrl": "" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Models/FileTransferStatusEventExt.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Enums; 2 | 3 | namespace Altinn.Broker.Core.Models; 4 | 5 | /// 6 | /// Represents the status of a file transfer. 7 | /// 8 | public class FileTransferStatusEventExt 9 | { 10 | /// 11 | /// The status code of the file transfer. 12 | /// 13 | public FileTransferStatusExt FileTransferStatus { get; set; } 14 | 15 | /// 16 | /// The status text of the file transfer. 17 | /// 18 | public string FileTransferStatusText { get; set; } = string.Empty; 19 | 20 | /// 21 | /// The date and time when the status of the file transfer was last changed. 22 | /// 23 | public DateTimeOffset FileTransferStatusChanged { get; set; } 24 | } 25 | -------------------------------------------------------------------------------- /.bruno/SystemRegister/Create recipient systemuser request.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Create recipient systemuser request 3 | type: http 4 | seq: 3 5 | } 6 | 7 | post { 8 | url: {{platform_base_url}}/authentication/api/v1/systemuser/request/vendor 9 | body: json 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: {{systemprovider_token}} 15 | } 16 | 17 | body:json { 18 | { 19 | "externalReference": "dev-test-create_01", 20 | "systemId": "{{serviceowner_orgnumber}}_{{resource_id}}_broker", 21 | "partyOrgNo": "{{recipient_orgnumber}}", 22 | "rights": [ 23 | { 24 | "Resource": [ 25 | { 26 | "id": "urn:altinn:resource", 27 | "value": "{{resource_id}}" 28 | } 29 | ] 30 | } 31 | ], 32 | "redirectUrl": "" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Related Issue(s) 7 | - #{issue number} 8 | 9 | ## Verification 10 | - [ ] **Your** code builds clean without any errors or warnings 11 | - [ ] Manual testing done (required) 12 | - [ ] Relevant automated test added (if you find this hard, leave it and we'll help out) 13 | - [ ] All tests run green 14 | - [ ] If pre- or post-deploy actions (including database migrations) are needed, add a description, include a "Pre/Post-deploy actions" section below, and mark the PR title with ⚠️ 15 | 16 | ## Documentation 17 | - [ ] User documentation is updated with a separate linked PR in [altinn-studio-docs.](https://github.com/Altinn/altinn-studio-docs) (if applicable) -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | name: Question ❓ 2 | description: Ask a question related to this product. 3 | labels: ["kind/question", "status/triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Unsure if you've found a bug or have a feature request? Use this template to ask us a question to find out. 9 | 10 | - type: textarea 11 | id: question 12 | attributes: 13 | label: Question 14 | description: How can we help you? What do you want to know? 15 | validations: 16 | required: true 17 | 18 | - type: textarea 19 | id: additional-information 20 | attributes: 21 | label: Additional Information 22 | description: | 23 | Add any other context, screenshots or code that might help us understanding and answering the question. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/epic--new-template-.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Epic (new template) 3 | about: Testing template creation 4 | title: '' 5 | labels: kind/epic, role/sender, status/draft 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Users view 11 | 12 | User role(s): Mottaker 13 | Users value statement(s): 14 | * Bullet list 15 | 16 | ## Vendors view 17 | ### Description 18 | #### High level features (capabilities): 19 | * Bullet list 20 | #### Additional information 21 | TBD 22 | 23 | ```[tasklist] 24 | ### Features 25 | - [ ] Example feature 26 | ``` 27 | 28 | ```[tasklist] 29 | ### Work items 30 | ``` 31 | 32 | ## Item attributes 33 | 34 | 35 | _Note: Automatically updated properties, not intended for change by you;)_ 36 | 37 | _Issue type: epic_ 38 | _Concept no: _ 39 | _Stage: E.g. Operation_ 40 | _ArchiConceptID: _ 41 | _GithubIssueID: _ 42 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Integrations/Azure/AzureStorageOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Options; 2 | 3 | /// 4 | /// Configuration options for Azure Storage block blob operations. 5 | /// Used primarily for controlling large file upload behavior. 6 | /// 7 | public class AzureStorageOptions 8 | { 9 | /// 10 | /// Size of each block in bytes. Must be between 1MB and 4000MB. 11 | /// 12 | public int BlockSize { get; set; } 13 | 14 | /// 15 | /// Number of concurrent threads for parallel upload operations. 16 | /// 17 | public int ConcurrentUploadThreads { get; set; } 18 | 19 | /// 20 | /// Number of blocks to upload before committing to Azure Storage. 21 | /// 22 | public int BlocksBeforeCommit { get; set; } 23 | } 24 | -------------------------------------------------------------------------------- /.bruno/FileTransfer/Initialize and upload (form-data).bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Initialize and upload (form-data) 3 | type: http 4 | seq: 3 5 | } 6 | 7 | post { 8 | url: {{broker_base_url}}/broker/api/v1/filetransfer/upload 9 | body: multipartForm 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: {{sender_token}} 15 | } 16 | 17 | body:multipart-form { 18 | Metadata.FileName: collection.bru 19 | Metadata.ResourceId: bruno-broker 20 | Metadata.SendersFileTransferReference: CaseFiles-123 21 | Metadata.Sender: 0192:{{sender_orgnumber}} 22 | Metadata.Recipients: 0192:{{recipient_orgnumber}} 23 | Metadata.DisableVirusScan: false 24 | FileTransfer: @file(collection.bru) 25 | } 26 | 27 | script:post-response { 28 | const responseData = res.body; 29 | bru.setVar("fileTransferId", responseData.fileTransferId); 30 | } 31 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Integrations/Azure/AzureResourceManagerOptions.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Altinn.Broker.Integrations.Azure; 4 | 5 | public class AzureResourceManagerOptions 6 | { 7 | public string Location { get; set; } = string.Empty; 8 | [StringLength(7, ErrorMessage = "The environment can only be 7 characters long because of constraint on length of Azure storage account name")] 9 | public string Environment { get; set; } = string.Empty; 10 | public string SubscriptionId { get; set; } = string.Empty; 11 | public string ApplicationResourceGroupName { get; set; } = string.Empty; 12 | public string MalwareScanEventGridTopicName { get; set; } = string.Empty; 13 | public string ContainerAppName { get; set; } = string.Empty; 14 | public string ApimIP { get; set; } = string.Empty; 15 | } 16 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/ResourceEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Domain; 2 | 3 | public class ResourceEntity 4 | { 5 | public required string Id { get; set; } 6 | public DateTimeOffset? Created { get; set; } 7 | public string? OrganizationNumber { get; set; } 8 | public required string ServiceOwnerId { get; set; } 9 | public long? MaxFileTransferSize { get; set; } 10 | public TimeSpan? FileTransferTimeToLive { get; set; } 11 | public bool PurgeFileTransferAfterAllRecipientsConfirmed { get; set; } = true; 12 | public TimeSpan? PurgeFileTransferGracePeriod { get; set; } 13 | public bool? UseManifestFileShim { get; set; } 14 | public string? ExternalServiceCodeLegacy { get; set; } 15 | public int? ExternalServiceEditionCodeLegacy { get; set; } 16 | public bool ApprovedForDisabledVirusScan { get; set; } = false; 17 | } 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request ✨ 2 | description: Request a new feature or enhancement 3 | labels: ["kind/feature-request", "status/triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Please make sure this feature request hasn't been already submitted by someone by looking through other open/closed issues 9 | 10 | - type: textarea 11 | id: description 12 | attributes: 13 | label: Description 14 | description: Give us a brief description of the feature or enhancement you would like 15 | validations: 16 | required: true 17 | 18 | - type: textarea 19 | id: additional-information 20 | attributes: 21 | label: Additional Information 22 | description: Give us some additional information on the feature request like proposed solutions, links, screenshots, etc. -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Repositories/IAuthorizationService.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | using Altinn.Broker.Core.Domain; 4 | 5 | namespace Altinn.Broker.Core.Repositories; 6 | public interface IAuthorizationService 7 | { 8 | Task CheckAccessAsSender(ClaimsPrincipal? user, string resourceId, string party, bool isLegacyUser, CancellationToken cancellationToken = default); 9 | Task CheckAccessAsSenderOrRecipient(ClaimsPrincipal? user, FileTransferEntity fileTransfer, bool isLegacyUser, CancellationToken cancellationToken = default); 10 | Task CheckAccessForSearch(ClaimsPrincipal? user, string resourceId, string party, bool isLegacyUser, CancellationToken cancellationToken = default); 11 | Task CheckAccessAsRecipient(ClaimsPrincipal? user, FileTransferEntity fileTransfer, bool isLegacyUser, CancellationToken cancellationToken = default); 12 | } 13 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Migrations/V0011__optimize_legacy_search.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX file_transfer_resource_id_idx ON broker.file_transfer (resource_id); 2 | CREATE INDEX actor_actor_external_id_idx ON broker.actor (actor_external_id); 3 | CREATE INDEX file_transfer_status_file_transfer_id_fk_idx ON broker.file_transfer_status (file_transfer_id_fk); 4 | CREATE INDEX actor_file_transfer_status_file_transfer_id_fk_idx ON broker.actor_file_transfer_status (file_transfer_id_fk); 5 | CREATE INDEX actor_file_transfer_status_actor_id_fk_idx ON broker.actor_file_transfer_status (actor_id_fk); 6 | CREATE INDEX actor_file_transfer_status_description_idx ON broker.actor_file_transfer_status (file_transfer_id_fk, actor_file_transfer_status_description_id_fk); 7 | CREATE INDEX file_transfer_status_status_description_id_fk_idx ON broker.file_transfer_status (file_transfer_status_description_id_fk, file_transfer_id_fk); 8 | -------------------------------------------------------------------------------- /.bruno/FileTransfer/Initialize.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Initialize 3 | type: http 4 | seq: 1 5 | } 6 | 7 | post { 8 | url: {{broker_base_url}}/broker/api/v1/filetransfer 9 | body: json 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: {{sender_token}} 15 | } 16 | 17 | body:json { 18 | { 19 | "fileName": "collection.bru", 20 | "resourceId": "{{resource_id}}", 21 | "sendersFileTransferReference": "archiveno-20425", 22 | "sender": "urn:altinn:organization:identifier-no:{{sender_orgnumber}}", 23 | "recipients": [ 24 | "urn:altinn:organization:identifier-no:{{recipient_orgnumber}}" 25 | ], 26 | "propertyList": { 27 | "caseNumber": "123", 28 | "etc": "can-use-for-anything" 29 | } 30 | } 31 | } 32 | 33 | script:post-response { 34 | const responseData = res.body; 35 | bru.setVar("fileTransferId", responseData.fileTransferId); 36 | } 37 | -------------------------------------------------------------------------------- /.bruno/Resource/Get resource configuration.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Get resource configuration 3 | type: http 4 | seq: 2 5 | } 6 | 7 | get { 8 | url: {{broker_base_url}}/broker/api/v1/resource/{{resource_id}} 9 | body: none 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: {{systemprovider_token}} 15 | } 16 | 17 | tests { 18 | test("Status code is 200", function() { 19 | expect(res.getStatus()).to.equal(200); 20 | }); 21 | 22 | test("Response contains resource configuration", function() { 23 | const body = res.getBody(); 24 | expect(body).to.have.property('MaxFileTransferSize'); 25 | expect(body).to.have.property('FileTransferTimeToLive'); 26 | expect(body).to.have.property('PurgeFileTransferAfterAllRecipientsConfirmed'); 27 | expect(body).to.have.property('PurgeFileTransferGracePeriod'); 28 | expect(body).to.have.property('UseManifestFileShim'); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Configuration/AuthorizationConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.API.Configuration; 2 | 3 | public static class AuthorizationConstants 4 | { 5 | public const string Sender = "Sender"; 6 | public const string Recipient = "Recipient"; 7 | public const string SenderOrRecipient = "SenderOrRecipient"; 8 | public const string Legacy = "Legacy"; 9 | public const string ServiceOwner = "ServiceOwner"; 10 | public const string Maintenance = "Maintenance"; 11 | public const string LegacyAndMaskinporten = "LegacyAndMaskinporten"; 12 | 13 | public const string SenderScope = "altinn:broker.write"; 14 | public const string RecipientScope = "altinn:broker.read"; 15 | public const string LegacyScope = "altinn:broker.legacy"; 16 | public const string ServiceOwnerScope = "altinn:serviceowner"; 17 | public const string MaintenanceScope = "altinn:broker.maintenance"; 18 | } 19 | -------------------------------------------------------------------------------- /.bruno/Resource/Configure resource for Broker.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Configure resource for Broker 3 | type: http 4 | seq: 1 5 | } 6 | 7 | put { 8 | url: {{broker_base_url}}/broker/api/v1/resource/{{resource_id}} 9 | body: json 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: {{systemprovider_token}} 15 | } 16 | 17 | body:json { 18 | { 19 | "MaxFileTransferSize": 2147483648, 20 | "FileTransferTimeToLive": "P30D", 21 | "PurgeFileTransferAfterAllRecipientsConfirmed": true, 22 | "PurgeFileTransferGracePeriod": "PT2H", 23 | "UseManifestFileShim": false, 24 | "ExternalServiceCodeLegacy": null, 25 | "ExternalServiceEditionCodeLegacy": null 26 | } 27 | } 28 | 29 | tests { 30 | test("Status code is 200", function() { 31 | expect(res.getStatus()).to.equal(200); 32 | }); 33 | 34 | test("Response is empty", function() { 35 | expect(res.getBody()).to.equal(''); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Helpers/FileTransferExtensions.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Common; 2 | using Altinn.Broker.Core.Domain; 3 | 4 | namespace Altinn.Broker.Core; 5 | public static class FileTransferExtensions 6 | { 7 | public static bool IsSender(this FileTransferEntity fileTransfer, string onBehalfOf) 8 | { 9 | return fileTransfer.Sender.ActorExternalId.WithoutPrefix() == onBehalfOf.WithoutPrefix(); 10 | } 11 | 12 | public static bool IsRecipient(this FileTransferEntity fileTransfer, string onBehalfOf) 13 | { 14 | return fileTransfer.RecipientCurrentStatuses.Any(recipientStatus => recipientStatus.Actor.ActorExternalId.WithoutPrefix() == onBehalfOf.WithoutPrefix()); 15 | } 16 | 17 | public static bool IsSenderOrRecipient(this FileTransferEntity fileTransfer, string onBehalfOf) 18 | { 19 | return fileTransfer.IsSender(onBehalfOf) || fileTransfer.IsRecipient(onBehalfOf); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Mappers/LegacyInitializeFileMapper.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Application.InitializeFileTransfer; 2 | using Altinn.Broker.Models; 3 | 4 | namespace Altinn.Broker.Mappers; 5 | 6 | internal static class LegacyInitializeFileMapper 7 | { 8 | internal static InitializeFileTransferRequest MapToRequest(LegacyFileInitalizeExt fileInitializeExt) 9 | { 10 | return new InitializeFileTransferRequest() 11 | { 12 | ResourceId = fileInitializeExt.ResourceId, 13 | FileName = fileInitializeExt.FileName, 14 | SenderExternalId = fileInitializeExt.Sender, 15 | SendersFileTransferReference = fileInitializeExt.SendersFileTransferReference, 16 | PropertyList = fileInitializeExt.PropertyList, 17 | RecipientExternalIds = fileInitializeExt.Recipients, 18 | Checksum = fileInitializeExt.Checksum, 19 | IsLegacy = true 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.azure/modules/storageAccount/create.bicep: -------------------------------------------------------------------------------- 1 | @secure() 2 | param migrationsStorageAccountName string 3 | param fileshare string 4 | param location string 5 | 6 | resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = { 7 | name: migrationsStorageAccountName 8 | location: location 9 | kind: 'StorageV2' 10 | sku: { 11 | name: 'Standard_LRS' 12 | } 13 | properties: { 14 | accessTier: 'Cool' 15 | minimumTlsVersion: 'TLS1_2' 16 | supportsHttpsTrafficOnly: true 17 | allowBlobPublicAccess: false 18 | } 19 | } 20 | 21 | resource storageAccountFileServices 'Microsoft.Storage/storageAccounts/fileServices@2023-05-01' = { 22 | name: 'default' 23 | parent: storageAccount 24 | } 25 | 26 | 27 | resource storageAccountFileShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2023-05-01' = { 28 | name: fileshare 29 | parent: storageAccountFileServices 30 | } 31 | 32 | output storageAccountId string = storageAccount.id 33 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Integrations/Altinn/Events/ConsoleLogEventBus.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Services; 2 | using Altinn.Broker.Core.Services.Enums; 3 | 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Altinn.Broker.Integrations.Altinn.Events; 7 | public class ConsoleLogEventBus(ILogger logger) : IEventBus 8 | { 9 | public Task Publish(AltinnEventType type, string resourceId, string fileTransferId, string? organizationId = null, Guid? guid = null, AltinnEventSubjectRole? subjectRole = null, CancellationToken cancellationToken = default) 10 | { 11 | logger.LogInformation( 12 | "{CloudEventType} event raised on instance {fileTransferId} for party with organization number {organizationId} and role {role}", 13 | type.ToString(), 14 | fileTransferId, 15 | organizationId, 16 | subjectRole?.ToString() ?? "Unknown"); 17 | return Task.CompletedTask; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.Tests/Data/MalwareScanResult_NoThreatFound.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "blobUri": "https://aitest0192991825827sa.blob.core.windows.net/brokerfiles/--FILEID--", 4 | "correlationId": "21c48159-e5ef-4376-ba96-4f8d6e0f1c7f", 5 | "eTag": "--ETAGID--", 6 | "scanFinishedTimeUtc": "2023-12-08T08:11:44.9457492Z", 7 | "scanResultDetails": null, 8 | "scanResultType": "No threats found" 9 | }, 10 | "dataVersion": "1.0", 11 | "eventTime": "2023-12-08T08:11:44.9464641Z", 12 | "eventType": "Microsoft.Security.MalwareScanningResult", 13 | "id": "21c48159-e5ef-4376-ba96-4f8d6e0f1c7f", 14 | "metadataVersion": "1", 15 | "subject": "storageAccounts/aitest0192991825827sa/containers/brokerfiles/blobs/--FILEID--", 16 | "topic": "/subscriptions/81cc3a6b-dfdf-49c7-96f0-3efddb159356/resourceGroups/serviceowner-test-0192-991825827-rg/providers/Microsoft.EventGrid/topics/test-broker-defenderresults" 17 | } -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/LegacyFileSearchEntity.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain.Enums; 2 | 3 | namespace Altinn.Broker.Core.Domain; 4 | 5 | public class LegacyFileSearchEntity 6 | { 7 | public ActorEntity? Actor { get; set; } 8 | public List? Actors { get; set; } 9 | public ActorFileTransferStatus? RecipientFileTransferStatus { get; set; } 10 | public FileTransferStatus? FileTransferStatus { get; set; } 11 | public DateTimeOffset? From { get; set; } 12 | public DateTimeOffset? To { get; set; } 13 | public string? ResourceId { get; set; } 14 | 15 | public long[] GetActorIds() 16 | { 17 | List actorIds = new(); 18 | if (Actor is not null) 19 | { 20 | actorIds.Add(Actor.ActorId); 21 | } 22 | 23 | if (Actors is not null) 24 | { 25 | actorIds.AddRange(Actors.Select(a => a.ActorId)); 26 | } 27 | 28 | return [.. actorIds]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Helpers/ValidateElementsInList.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Altinn.Broker.Helpers; 4 | 5 | public class ValidateElementsInList(Type attributeType, params object[] attributeArgs) : ValidationAttribute 6 | { 7 | protected override ValidationResult IsValid(object? value, ValidationContext validationContext) 8 | { 9 | var list = value as IEnumerable; 10 | if (list != null) 11 | { 12 | var attributeInstance = (ValidationAttribute)Activator.CreateInstance(attributeType, attributeArgs)!; 13 | attributeInstance.ErrorMessage = this.ErrorMessage; 14 | foreach (var item in list) 15 | { 16 | if (!attributeInstance.IsValid(item)) 17 | { 18 | return new ValidationResult(attributeInstance.ErrorMessage); 19 | } 20 | } 21 | } 22 | 23 | return ValidationResult.Success!; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.bruno/FileTransfer/{fileTransferId}/Details.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Details 3 | type: http 4 | seq: 4 5 | } 6 | 7 | get { 8 | url: {{broker_base_url}}/broker/api/v1/filetransfer/{{fileTransferId}}/details 9 | body: none 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: {{sender_token}} 15 | } 16 | 17 | tests { 18 | test("Status code is 200", function() { 19 | expect(res.getStatus()).to.equal(200); 20 | }); 21 | 22 | test("Response contains file transfer details", function() { 23 | const body = res.getBody(); 24 | expect(body).to.have.property('fileTransferId'); 25 | expect(body).to.have.property('fileTransferStatus'); 26 | expect(body).to.have.property('fileTransferStatusHistory'); 27 | expect(body).to.have.property('recipientFileTransferStatusHistory'); 28 | }); 29 | 30 | test("File transfer ID matches request", function() { 31 | const body = res.getBody(); 32 | expect(body.fileTransferId).to.equal(req.getEnvironmentVar('fileTransferId')); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/ValidationAttributes/ResourceIdentifierAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Text.RegularExpressions; 3 | using Altinn.Broker.Common.Constants; 4 | 5 | namespace Altinn.Broker.API.ValidationAttributes; 6 | 7 | public class ResourceIdentifierAttribute : ValidationAttribute 8 | { 9 | private static readonly string Pattern = $@"^(?:{UrnConstants.Resource}:)?[^:]{{1,255}}$"; 10 | private static readonly Regex Regex = new(Pattern); 11 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) 12 | { 13 | if (value is not string stringValue || !IsValidResourceFormat(stringValue)) 14 | { 15 | return new ValidationResult(ErrorMessage ?? "Invalid Resource identifier format"); 16 | } 17 | 18 | return ValidationResult.Success; 19 | } 20 | 21 | public static bool IsValidResourceFormat(string value) 22 | { 23 | return string.IsNullOrEmpty(value) || Regex.IsMatch(value); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Models/Recipient/RecipientFileStatusEventExt.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Enums; 2 | 3 | namespace Altinn.Broker.Models; 4 | 5 | /// 6 | /// Represents the status of a file transfer to a recipient. 7 | /// 8 | public class RecipientFileTransferStatusEventExt 9 | { 10 | /// 11 | /// The recipient of the file transfer. 12 | /// 13 | public string Recipient { get; set; } = string.Empty; 14 | 15 | /// 16 | /// The status code of the file transfer. 17 | /// 18 | public RecipientFileTransferStatusExt RecipientFileTransferStatusCode { get; set; } 19 | 20 | /// 21 | /// The status text of the file transfer. 22 | /// 23 | public string RecipientFileTransferStatusText { get; set; } = string.Empty; 24 | 25 | /// 26 | /// The date and time when the status of the file transfer was last changed. 27 | /// 28 | public DateTimeOffset RecipientFileTransferStatusChanged { get; set; } 29 | } 30 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Common/Models/SystemUserAuthorizationDetails.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Altinn.Broker.Common.Helpers.Models; 4 | public class SystemUserAuthorizationDetails 5 | { 6 | [JsonPropertyName("type")] 7 | public required string Type { get; set; } 8 | 9 | [JsonPropertyName("systemuser_id")] 10 | public required List SystemUserId { get; set; } 11 | 12 | [JsonPropertyName("systemuser_org")] 13 | public required SystemUserOrg SystemUserOrg { get; set; } 14 | 15 | [JsonPropertyName("system_id")] 16 | public required string SystemId { get; set; } 17 | } 18 | 19 | public class SystemUserAuthorization 20 | { 21 | [JsonPropertyName("authorization_details")] 22 | public List AuthorizationDetails { get; set; } 23 | } 24 | 25 | public class SystemUserOrg 26 | { 27 | [JsonPropertyName("authority")] 28 | public required string Authority { get; set; } 29 | 30 | [JsonPropertyName("ID")] 31 | public required string ID { get; set; } 32 | } 33 | -------------------------------------------------------------------------------- /.github/actions/publish-image/action.yml: -------------------------------------------------------------------------------- 1 | name: Publish image 2 | 3 | description: "Publishes a docker image to GitHub Container Registry" 4 | 5 | inputs: 6 | dockerImageBaseName: 7 | description: "Base image name for docker images" 8 | required: true 9 | imageTag: 10 | description: "Tag for the image" 11 | required: true 12 | GITHUB_TOKEN: 13 | description: "GitHub token" 14 | required: true 15 | default: ${{ github.token }} 16 | 17 | runs: 18 | using: "composite" 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Login to GitHub Container Registry 22 | uses: docker/login-action@v3 23 | with: 24 | registry: ghcr.io 25 | username: ${{ github.actor }} 26 | password: ${{ inputs.GITHUB_TOKEN }} 27 | 28 | - name: Push image to registry 29 | shell: bash 30 | run: | 31 | # Construct the image tag using the Git hash 32 | IMAGE="${{ inputs.dockerImageBaseName }}:${{ inputs.imageTag }}" 33 | docker build . --tag $IMAGE 34 | docker push $IMAGE 35 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Integrations/Azure/MalwareScanConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Integrations.Azure; 2 | 3 | internal class MalwareScanConfiguration 4 | { 5 | public required Properties Properties { get; set; } 6 | public required string Scope { get; set; } 7 | } 8 | 9 | internal class MalwareScanning 10 | { 11 | public required OnUpload OnUpload { get; set; } 12 | public required string ScanResultsEventGridTopicResourceId { get; set; } 13 | } 14 | 15 | internal class OnUpload 16 | { 17 | public required bool IsEnabled { get; set; } 18 | public required int CapGBPerMonth { get; set; } 19 | } 20 | 21 | internal class Properties 22 | { 23 | public required bool IsEnabled { get; set; } 24 | public required MalwareScanning MalwareScanning { get; set; } 25 | public required SensitiveDataDiscovery SensitiveDataDiscovery { get; set; } 26 | public required bool OverrideSubscriptionLevelSettings { get; set; } 27 | } 28 | 29 | internal class SensitiveDataDiscovery 30 | { 31 | public required bool IsEnabled { get; set; } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Models/Recipient/RecipientFileStatusDetailsExt.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Enums; 2 | 3 | namespace Altinn.Broker.Models; 4 | 5 | /// 6 | /// Represents the current status of a file transfer for a specific recipient. 7 | /// 8 | public class RecipientFileTransferStatusDetailsExt 9 | { 10 | /// 11 | /// The recipient of the file transfer. 12 | /// 13 | public string Recipient { get; set; } = string.Empty; 14 | 15 | /// 16 | /// The current status code of the file transfer. 17 | /// 18 | public RecipientFileTransferStatusExt CurrentRecipientFileTransferStatusCode { get; set; } 19 | 20 | /// 21 | /// The current status text of the file transfer. 22 | /// 23 | public string CurrentRecipientFileTransferStatusText { get; set; } = string.Empty; 24 | 25 | /// 26 | /// The date and time when the status of the file transfer was last changed. 27 | /// 28 | public DateTimeOffset CurrentRecipientFileTransferStatusChanged { get; set; } 29 | } 30 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/GetResource/GetResourceHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | using Altinn.Broker.Common; 4 | using Altinn.Broker.Core.Application; 5 | using Altinn.Broker.Core.Domain; 6 | using Altinn.Broker.Core.Repositories; 7 | 8 | using OneOf; 9 | 10 | namespace Altinn.Broker.Application.GetResource; 11 | public class GetResourceHandler(IResourceRepository resourceRepository) : IHandler 12 | { 13 | public async Task> Process(string resourceId, ClaimsPrincipal? user, CancellationToken cancellationToken) 14 | { 15 | var resource = await resourceRepository.GetResource(resourceId, cancellationToken); 16 | if (resource is null) 17 | { 18 | return Errors.NoAccessToResource; 19 | } 20 | var serviceOwner = user.GetCallerOrganizationId(); 21 | if (resource.OrganizationNumber.WithoutPrefix() != user.GetCallerOrganizationId().WithoutPrefix()) 22 | { 23 | return Errors.NoAccessToResource; 24 | } 25 | 26 | return resource; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.azure/modules/dbBackupStorage/create.bicep: -------------------------------------------------------------------------------- 1 | @secure() 2 | param backupStorageAccountName string 3 | param location string 4 | param containerName string = 'database-backups' 5 | 6 | resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = { 7 | name: backupStorageAccountName 8 | location: location 9 | kind: 'StorageV2' 10 | sku: { 11 | name: 'Standard_LRS' 12 | } 13 | properties: { 14 | accessTier: 'Cool' 15 | minimumTlsVersion: 'TLS1_2' 16 | supportsHttpsTrafficOnly: true 17 | allowBlobPublicAccess: false 18 | } 19 | } 20 | 21 | resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-05-01' = { 22 | name: 'default' 23 | parent: storageAccount 24 | } 25 | 26 | resource blobContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-05-01' = { 27 | name: containerName 28 | parent: blobService 29 | properties: { 30 | publicAccess: 'None' 31 | } 32 | } 33 | 34 | output storageAccountId string = storageAccount.id 35 | output storageAccountName string = storageAccount.name 36 | output containerName string = blobContainer.name 37 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.Tests.LargeFile/Altinn.Broker.Tests.LargeFile.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net9.0 5 | enable 6 | enable 7 | Linux 8 | ..\.. 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.azure/bicepconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://aka.ms/bicep/config for more information on Bicep configuration options 3 | // Press CTRL+SPACE/CMD+SPACE at any location to see Intellisense suggestions 4 | "analyzers": { 5 | "core": { 6 | "rules": { 7 | "no-unused-params": { 8 | "level": "warning" 9 | }, 10 | "no-unused-vars": { 11 | "level": "warning" 12 | }, 13 | "no-hardcoded-env-urls": { 14 | "level": "warning" 15 | }, 16 | "secure-secrets-in-params": { 17 | "level": "warning" 18 | }, 19 | "no-unnecessary-dependson": { 20 | "level": "warning" 21 | }, 22 | "outputs-should-not-contain-secrets": { 23 | "level": "warning" 24 | } 25 | } 26 | } 27 | }, 28 | "experimentalFeaturesEnabled": { 29 | "compileTimeImports": true, 30 | "userDefinedFunctions": false 31 | } 32 | } -------------------------------------------------------------------------------- /.azure/modules/containerApp/fetchEventGridIps.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | 3 | @secure() 4 | param principal_id string 5 | 6 | resource deploymentScript 'Microsoft.Resources/deploymentScripts@2023-08-01' = { 7 | name: 'fetchAzureEventGridIpsScript' 8 | location: location 9 | kind: 'AzurePowerShell' 10 | identity: { 11 | type: 'UserAssigned' 12 | userAssignedIdentities: { 13 | '${principal_id}': {} 14 | } 15 | } 16 | properties: { 17 | azPowerShellVersion: '13.0' 18 | scriptContent: ''' 19 | param([string] $location) 20 | $serviceTags = Get-AzNetworkServiceTag -Location $location 21 | $EventgridIps = $serviceTags.Values | Where-Object { $_.Name -eq "AzureEventGrid" } 22 | $output = $EventgridIps.Properties.AddressPrefixes | Where-Object { $_ -notmatch ":" } 23 | $DeploymentScriptOutputs = @{} 24 | $DeploymentScriptOutputs['eventGridIps'] = $output 25 | ''' 26 | arguments: '-location ${location}' 27 | forceUpdateTag: '1' 28 | retentionInterval: 'PT2H' 29 | } 30 | } 31 | 32 | output eventGridIps array = deploymentScript.properties.outputs.eventGridIps 33 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/ValidationAttributes/MD5ChecksumAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Altinn.Broker.API.ValidationAttributes 4 | { 5 | internal class MD5ChecksumAttribute : ValidationAttribute 6 | { 7 | public MD5ChecksumAttribute() 8 | { 9 | } 10 | 11 | protected override ValidationResult IsValid(object? value, ValidationContext validationContext) 12 | { 13 | var stringValue = value as string; 14 | if (string.IsNullOrWhiteSpace(stringValue)) 15 | { 16 | return ValidationResult.Success!; 17 | } 18 | if (stringValue.Length != 32) 19 | { 20 | return new ValidationResult("The checksum, if used, must be a MD5 hash with a length of 32 characters"); 21 | } 22 | if (stringValue.ToLowerInvariant() != stringValue) 23 | { 24 | return new ValidationResult("The checksum, if used, must be a MD5 hash in lower case"); 25 | } 26 | return ValidationResult.Success!; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Models/FileTransferStatusDetailsExt.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | using Altinn.Broker.Models; 4 | 5 | namespace Altinn.Broker.Core.Models; 6 | /// 7 | /// Overview of a broker file transfer which also includes the status history of the file transfer.| 8 | /// 9 | public class FileTransferStatusDetailsExt : FileTransferOverviewExt 10 | { 11 | /// 12 | /// The status history of the file transfer. 13 | /// 14 | public List FileTransferStatusHistory { get; set; } = new List(); 15 | 16 | /// 17 | /// The status history of the file transfer for each recipient. 18 | /// 19 | public List RecipientFileTransferStatusHistory { get; set; } = new List(); 20 | 21 | /// 22 | /// Hide the Published property from the details response since it's redundant with FileTransferStatusHistory. 23 | /// 24 | [JsonIgnore] 25 | public new DateTimeOffset? Published { get; set; } 26 | } 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:9.0.200-alpine3.20 AS build 2 | WORKDIR /app 3 | 4 | # Copy csproj and restore as distinct layers 5 | COPY src/Altinn.Broker.API/*.csproj ./src/Altinn.Broker.API/ 6 | COPY src/Altinn.Broker.Core/*.csproj ./src/Altinn.Broker.Core/ 7 | COPY src/Altinn.Broker.Integrations/*.csproj ./src/Altinn.Broker.Integrations/ 8 | COPY src/Altinn.Broker.Persistence/*.csproj ./src/Altinn.Broker.Persistence/ 9 | RUN dotnet restore ./src/Altinn.Broker.API/Altinn.Broker.API.csproj 10 | 11 | # Copy everything else and build 12 | COPY src ./src 13 | RUN dotnet publish -c Release -o out ./src/Altinn.Broker.API/Altinn.Broker.API.csproj 14 | 15 | # Build runtime image 16 | FROM mcr.microsoft.com/dotnet/aspnet:9.0.2-alpine3.20 AS final 17 | WORKDIR /app 18 | EXPOSE 2525 19 | ENV ASPNETCORE_URLS=http://+:2525 20 | 21 | COPY --from=build /app/out . 22 | #COPY src/Altinn.Broker.Persistence/Migration ./Migration 23 | 24 | RUN addgroup -g 3000 dotnet && adduser -u 1000 -G dotnet -D -s /bin/false dotnet 25 | 26 | RUN mkdir -p /mnt/storage 27 | run chown -R dotnet:dotnet /mnt/storage 28 | USER dotnet 29 | ENTRYPOINT [ "dotnet", "Altinn.Broker.API.dll" ] 30 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Mappers/InitializeFileTransferMapper.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Application.InitializeFileTransfer; 2 | using Altinn.Broker.Common; 3 | using Altinn.Broker.Core.Domain; 4 | using Altinn.Broker.Models; 5 | 6 | namespace Altinn.Broker.Mappers; 7 | 8 | internal static class InitializeFileTransferMapper 9 | { 10 | internal static InitializeFileTransferRequest MapToRequest(FileTransferInitalizeExt fileTransferInitializeExt) 11 | { 12 | return new InitializeFileTransferRequest() 13 | { 14 | ResourceId = fileTransferInitializeExt.ResourceId.WithoutPrefix(), 15 | FileName = fileTransferInitializeExt.FileName, 16 | SenderExternalId = fileTransferInitializeExt.Sender, 17 | SendersFileTransferReference = fileTransferInitializeExt.SendersFileTransferReference, 18 | PropertyList = fileTransferInitializeExt.PropertyList, 19 | RecipientExternalIds = fileTransferInitializeExt.Recipients, 20 | Checksum = fileTransferInitializeExt.Checksum, 21 | DisableVirusScan = fileTransferInitializeExt.DisableVirusScan ?? false 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/actions/get-current-version/action.yml: -------------------------------------------------------------------------------- 1 | name: "Get current version" 2 | 3 | description: "Get the current version from a file and the git short sha" 4 | 5 | outputs: 6 | version: 7 | description: "Version" 8 | value: ${{ steps.set-current-version.outputs.version }} 9 | gitShortSha: 10 | description: "Git short sha" 11 | value: ${{ steps.set-git-short-sha.outputs.gitShortSha }} 12 | imageTag: 13 | description: "Image tag" 14 | value: ${{ steps.set-image-tag.outputs.imageTag }} 15 | 16 | runs: 17 | using: "composite" 18 | steps: 19 | - name: Checkout GitHub Action" 20 | uses: actions/checkout@v4 21 | 22 | - name: Set current version 23 | id: set-current-version 24 | shell: bash 25 | run: echo "version=$(cat version.txt)" >> $GITHUB_OUTPUT 26 | 27 | - name: Set git short sha 28 | id: set-git-short-sha 29 | shell: bash 30 | run: echo "gitShortSha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT 31 | 32 | - name: Set image tag 33 | id: set-image-tag 34 | shell: bash 35 | run: echo "imageTag=$(cat version.txt)-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT 36 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.Tests/Data/MalwareScanResult_Malicious.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "blobUri": "https://aitest0192991825827sa.blob.core.windows.net/brokerfiles/--FILEID--", 4 | "correlationId": "2ee9f258-c96a-4982-9e6e-16b8485d71da", 5 | "eTag": "--ETAGID--", 6 | "scanFinishedTimeUtc": "2023-12-08T08:12:31.9933275Z", 7 | "scanResultDetails": { 8 | "malwareNamesFound": [ 9 | "Virus:DOS/EICAR_Test_File" 10 | ], 11 | "sha256": "275A021BBFB6489E54D471899F7DB9D1663FC695EC2FE2A2C4538AABF651FD0F" 12 | }, 13 | "scanResultType": "Malicious" 14 | }, 15 | "dataVersion": "1.0", 16 | "eventTime": "2023-12-08T08:12:31.9939079Z", 17 | "eventType": "Microsoft.Security.MalwareScanningResult", 18 | "id": "2ee9f258-c96a-4982-9e6e-16b8485d71da", 19 | "metadataVersion": "1", 20 | "subject": "storageAccounts/aitest0192991825827sa/containers/brokerfiles/blobs/--FILEID--", 21 | "topic": "/subscriptions/81cc3a6b-dfdf-49c7-96f0-3efddb159356/resourceGroups/serviceowner-test-0192-991825827-rg/providers/Microsoft.EventGrid/topics/test-broker-defenderresults" 22 | } -------------------------------------------------------------------------------- /.bruno/ServiceOwner/Get service owner configuration.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Get service owner configuration 3 | type: http 4 | seq: 2 5 | } 6 | 7 | get { 8 | url: {{broker_base_url}}/broker/api/v1/serviceowner 9 | body: none 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: {{systemprovider_token}} 15 | } 16 | 17 | tests { 18 | test("Status code is 200", function() { 19 | expect(res.getStatus()).to.equal(200); 20 | }); 21 | 22 | test("Response contains service owner information", function() { 23 | const body = res.getBody(); 24 | expect(body).to.have.property('Name'); 25 | expect(body).to.have.property('StorageProviders'); 26 | expect(body.StorageProviders).to.be.an('array'); 27 | }); 28 | 29 | test("Storage providers have required fields", function() { 30 | const body = res.getBody(); 31 | if (body.StorageProviders && body.StorageProviders.length > 0) { 32 | const provider = body.StorageProviders[0]; 33 | expect(provider).to.have.property('Type'); 34 | expect(provider).to.have.property('DeploymentStatus'); 35 | expect(provider).to.have.property('DeploymentEnvironment'); 36 | } 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.UseCaseTests/helpers/cleanupUseCaseTestsData.js: -------------------------------------------------------------------------------- 1 | import { getMaintenanceMaskinportenToken } from './maskinportenTokenService.js'; 2 | import { check } from 'k6'; 3 | import http from 'k6/http'; 4 | 5 | const baseUrl = __ENV.base_url; 6 | 7 | export async function cleanupUseCaseTestData(testTag) { 8 | const token = await getMaintenanceMaskinportenToken(); 9 | check(token, { 'Maintenance Maskinporten token obtained for cleanup': t => typeof t === 'string' && t.length > 0 }); 10 | 11 | const headers = { 12 | Authorization: `Bearer ${token}` 13 | }; 14 | 15 | const res = http.post(`${baseUrl}/broker/api/v1/maintenance/cleanup-usecasetests?testTag=${encodeURIComponent(testTag)}`, null, { headers }); 16 | check(res, { 'Cleanup use case test data status 200': r => r.status === 200 }); 17 | 18 | if (res.status === 200) { 19 | const body = res.json(); 20 | console.log(`Cleanup summary: resourceId=${body.resourceId}, testTag=${body.testTag}, fileTransfersFound=${body.fileTransfersFound}, deleteFileTransfersJobId=${body.deleteFileTransfersJobId}`); 21 | } else { 22 | console.error(`Cleanup failed. Status: ${res.status}. Body: ${res.body}`); 23 | } 24 | } -------------------------------------------------------------------------------- /.azure/modules/identity/addDeploymentRoles.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | param userAssignedIdentityPrincipalId string 4 | 5 | var userAccessAdminRoleDefinitionId = '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9' 6 | 7 | resource userAccessRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 8 | name: guid(subscription().id, userAssignedIdentityPrincipalId, userAccessAdminRoleDefinitionId) 9 | properties: { 10 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', userAccessAdminRoleDefinitionId) 11 | principalId: userAssignedIdentityPrincipalId 12 | principalType: 'ServicePrincipal' 13 | } 14 | } 15 | 16 | var contributorRoleDefinitionId = 'b24988ac-6180-42a0-ab88-20f7382dd24c' 17 | resource contributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 18 | name: guid(subscription().id, userAssignedIdentityPrincipalId, contributorRoleDefinitionId) 19 | properties: { 20 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', contributorRoleDefinitionId) 21 | principalId: userAssignedIdentityPrincipalId 22 | principalType: 'ServicePrincipal' 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/actions/release-to-git/action.yml: -------------------------------------------------------------------------------- 1 | name: Create release in github 2 | 3 | description: Create a release to git if the version has been bumped in the version.txt file 4 | 5 | inputs: 6 | GITHUB_TOKEN: 7 | description: "GitHub token" 8 | required: true 9 | default: ${{ github.token }} 10 | 11 | runs: 12 | using: "composite" 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Get version 17 | id: get-version 18 | uses: ./.github/actions/get-current-version 19 | 20 | - name: fetch tags 21 | shell: bash 22 | run: git fetch --tags 23 | 24 | - name: set latest tag 25 | shell: bash 26 | id: set-latest-tag 27 | run: | 28 | echo "latestTag=$(git tag | sort --version-sort | tail -n1)" >> $GITHUB_OUTPUT 29 | 30 | - name: Create release 31 | shell: bash 32 | if: ${{ !(steps.set-latest-tag.outputs.latestTag == steps.get-version.outputs.version) }} 33 | env: 34 | GITHUB_TOKEN: ${{ inputs.GITHUB_TOKEN }} 35 | TAG: ${{ steps.get-version.outputs.version }} 36 | run: | 37 | gh release create "$TAG" --title="v${{steps.get-version.outputs.version}}" --generate-notes 38 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Domain/FileTransferEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Core.Domain; 2 | 3 | public class FileTransferEntity 4 | { 5 | public required Guid FileTransferId { get; set; } 6 | public required string ResourceId { get; set; } 7 | public required ActorEntity Sender { get; set; } // Joined in 8 | public string? SendersFileTransferReference { get; set; } 9 | public required FileTransferStatusEntity FileTransferStatusEntity { get; set; } // Joined in 10 | public DateTimeOffset FileTransferStatusChanged { get; set; } 11 | public required DateTimeOffset Created { get; set; } 12 | public required DateTimeOffset ExpirationTime { get; set; } 13 | public required List RecipientCurrentStatuses { get; set; } // Joined in 14 | public string? FileLocation { get; set; } 15 | public string? HangfireJobId { get; set; } 16 | public required string FileName { get; set; } 17 | public long FileTransferSize { get; set; } = 0; 18 | public string? Checksum { get; set; } 19 | public bool UseVirusScan { get; set; } 20 | public Dictionary PropertyList { get; set; } = new Dictionary(); 21 | } 22 | -------------------------------------------------------------------------------- /.azure/modules/keyvault/addReaderRoles.bicep: -------------------------------------------------------------------------------- 1 | param keyvaultName string 2 | param principals array 3 | 4 | var secretsUserRoleId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') 5 | var keyVaultReaderRoleId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '21090545-7ca7-4776-b22c-e363652d74d2') 6 | 7 | resource keyvault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { 8 | name: keyvaultName 9 | } 10 | 11 | resource secretsUsers 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for p in principals: { 12 | name: guid(subscription().id, keyvault.id, p.objectId, secretsUserRoleId) 13 | scope: keyvault 14 | properties: { 15 | roleDefinitionId: secretsUserRoleId 16 | principalId: p.objectId 17 | principalType: p.principalType 18 | } 19 | }] 20 | 21 | resource kvReaders 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for p in principals: { 22 | name: guid(subscription().id, keyvault.id, p.objectId, keyVaultReaderRoleId) 23 | scope: keyvault 24 | properties: { 25 | roleDefinitionId: keyVaultReaderRoleId 26 | principalId: p.objectId 27 | principalType: p.principalType 28 | } 29 | }] 30 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Services/IResourceManager.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain; 2 | 3 | using Azure.ResourceManager.Network.Models; 4 | 5 | namespace Altinn.Broker.Core.Services; 6 | public interface IResourceManager 7 | { 8 | Task GetDeploymentStatus(StorageProviderEntity storageProviderEntity, CancellationToken cancellationToken); 9 | 10 | /// 11 | /// Deploys the required resources for the ServiceOwner. Must be idempotent. 12 | /// 13 | /// 14 | /// 15 | Task Deploy(ServiceOwnerEntity serviceOwnerEntity, bool virusScan, CancellationToken cancellationToken); 16 | 17 | void CreateStorageProviders(ServiceOwnerEntity serviceOwnerEntity, CancellationToken cancellationToken); 18 | 19 | Task GetStorageConnectionString(StorageProviderEntity storageProviderEntity); 20 | Task UpdateContainerAppIpRestrictionsAsync(Dictionary newIps, CancellationToken cancellationToken); 21 | Task RetrieveServiceTags(CancellationToken cancellationToken); 22 | Task> RetrieveCurrentIpRanges(CancellationToken cancellationToken); 23 | } 24 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/Altinn.Broker.Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.azure/infrastructure/params.bicepparam: -------------------------------------------------------------------------------- 1 | using './main.bicep' 2 | 3 | param namePrefix = readEnvironmentVariable('NAME_PREFIX') 4 | param location = 'norwayeast' 5 | param environment = readEnvironmentVariable('ENVIRONMENT') 6 | 7 | // secrets 8 | param brokerPgAdminPassword = readEnvironmentVariable('BROKER_PG_ADMIN_PASSWORD') 9 | param tenantId = readEnvironmentVariable('TENANT_ID') 10 | param test_client_id = readEnvironmentVariable('TEST_CLIENT_ID') 11 | param sourceKeyVaultName = readEnvironmentVariable('KEY_VAULT_NAME') 12 | param migrationsStorageAccountName = readEnvironmentVariable('MIGRATION_STORAGE_ACCOUNT_NAME') 13 | param backupStorageAccountName = readEnvironmentVariable('BACKUP_STORAGE_ACCOUNT_NAME') 14 | param maskinportenJwk = readEnvironmentVariable('MASKINPORTEN_JWK') 15 | param maskinportenClientId = readEnvironmentVariable('MASKINPORTEN_CLIENT_ID') 16 | param platformSubscriptionKey = readEnvironmentVariable('PLATFORM_SUBSCRIPTION_KEY') 17 | param slackUrl = readEnvironmentVariable('SLACK_URL') 18 | param statisticsApiKey = readEnvironmentVariable('STATISTICS_API_KEY') 19 | param grafanaMonitoringPrincipalId = readEnvironmentVariable('GRAFANA_MONITORING_PRINCIPAL_ID') 20 | 21 | // SKUs 22 | param keyVaultSku = { 23 | name: 'standard' 24 | family: 'A' 25 | } 26 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Services/IBrokerStorageService.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain; 2 | 3 | /// 4 | /// Handles the interplay of the ServiceOwnerEntity and the infrastructure resources we manage for them 5 | /// 6 | public interface IBrokerStorageService 7 | { 8 | /// 9 | /// Looks up the correct storage account to use for service owner and upload the file 10 | /// 11 | /// The service owner entity. 12 | /// The stream to upload. 13 | /// A string containing the MD5 checksum. Null if failure. 14 | Task UploadFile(ServiceOwnerEntity serviceOwnerEntity, FileTransferEntity fileTransferEntity, Stream stream, long streamLength, CancellationToken cancellationToken); 15 | 16 | Task DownloadFile(ServiceOwnerEntity serviceOwnerEntity, FileTransferEntity fileTransfer, CancellationToken cancellationToken); 17 | Task DeleteFile(ServiceOwnerEntity serviceOwnerEntity, FileTransferEntity fileTransferEntity, CancellationToken cancellationToken); 18 | Task SetContentHashForExistingBlob(ServiceOwnerEntity serviceOwnerEntity, FileTransferEntity fileTransferEntity, CancellationToken cancellationToken); 19 | } 20 | -------------------------------------------------------------------------------- /.bruno/jwt-helper.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | 3 | function generateJWT(clientId, clientKid, clientPem, scope, authDetails = null) { 4 | const header = { 5 | "alg": "RS256", 6 | "kid": clientKid 7 | }; 8 | 9 | const payload = { 10 | "aud": "https://test.maskinporten.no/", 11 | "scope": scope, 12 | "iss": clientId, 13 | "iat": Math.floor(Date.now() / 1000), 14 | "exp": Math.floor(Date.now() / 1000) + 120 15 | }; 16 | 17 | if (authDetails) { 18 | payload.authorization_details = authDetails; 19 | } 20 | 21 | function base64url(input) { 22 | return Buffer.from(input).toString('base64') 23 | .replace(/\+/g, '-') 24 | .replace(/\//g, '_') 25 | .replace(/=/g, ''); 26 | } 27 | 28 | const encodedHeader = base64url(JSON.stringify(header)); 29 | const encodedPayload = base64url(JSON.stringify(payload)); 30 | const signatureInput = `${encodedHeader}.${encodedPayload}`; 31 | 32 | const signature = crypto.sign("RSA-SHA256", Buffer.from(signatureInput), clientPem); 33 | const encodedSignature = base64url(signature); 34 | 35 | return `${signatureInput}.${encodedSignature}`; 36 | } 37 | 38 | module.exports = { generateJWT }; 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 🐛 2 | description: File a bug report here 3 | labels: ["kind/bug", "status/triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this bug report 🤗 9 | Make sure there aren't any open/closed issues for this topic 😃 10 | 11 | - type: textarea 12 | id: bug-description 13 | attributes: 14 | label: Description of the bug 15 | description: Give us a brief description of what happened and what should have happened 16 | validations: 17 | required: true 18 | 19 | - type: textarea 20 | id: steps-to-reproduce 21 | attributes: 22 | label: Steps To Reproduce 23 | description: Steps to reproduce the behavior. 24 | placeholder: | 25 | 1. Go to '...' 26 | 2. Click on '...' 27 | 3. Scroll down to '...' 28 | 4. See error 29 | validations: 30 | required: true 31 | - type: textarea 32 | id: additional-information 33 | attributes: 34 | label: Additional Information 35 | description: | 36 | Provide any additional information such as logs, screenshots, likes, scenarios in which the bug occurs so that it facilitates resolving the issue. -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Altinn.Broker.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.Tests/Data/altinn-broker-test-resource-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "altinn-broker-test-resource-1", 3 | "title": { 4 | "en": "Altinn Broker - Delete", 5 | "nb-no": "Altinn Broker - Slett", 6 | "nn-no": "Altinn Broker - Slett" 7 | }, 8 | "description": { 9 | "en": "Altinn Broker test resource", 10 | "nb-no": "Altinn Broker testressurs", 11 | "nn-no": "Altinn Broker testressurs" 12 | }, 13 | "rightDescription": { 14 | "en": "Access to Altinn Broker test resource 1", 15 | "nb-no": "Tilgang til Altinn Broker testressurs 1", 16 | "nn-no": "Tilgang til Altinn Broker testressurs 1" 17 | }, 18 | "homepage": "https://www.digdir.no/", 19 | "status": "Active", 20 | "contactPoints": [], 21 | "isPartOf": "Altinn", 22 | "resourceReferences": [], 23 | "delegable": false, 24 | "visible": false, 25 | "hasCompetentAuthority": { 26 | "organization": "991825827", 27 | "orgcode": "TTD", 28 | "name": { 29 | "en": "Testdepartementet", 30 | "nb-no": "Testdepartementet", 31 | "nn-no": "Testdepartementet" 32 | } 33 | }, 34 | "keywords": [], 35 | "limitedByRRR": false, 36 | "selfIdentifiedUserEnabled": false, 37 | "enterpriseUserEnabled": false, 38 | "resourceType": "GenericAccessResource" 39 | } 40 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:37623", 8 | "sslPort": 44338 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5096", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7241;http://localhost:5096", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.azure/modules/policy/assignBrokerTags.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | param policyDefinitionId string 4 | param userAssignedIdentityName string 5 | 6 | resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 7 | name: userAssignedIdentityName 8 | location: resourceGroup().location 9 | } 10 | 11 | resource brokerTagsAssignment 'Microsoft.Authorization/policyAssignments@2025-03-01' = { 12 | name: 'broker-standard-tags' 13 | location: resourceGroup().location 14 | identity: { 15 | type: 'UserAssigned' 16 | userAssignedIdentities: { 17 | '${userAssignedIdentity.id}': {} 18 | } 19 | } 20 | properties: { 21 | displayName: 'Ensure standard tags on Broker resources' 22 | policyDefinitionId: policyDefinitionId 23 | enforcementMode: 'Default' 24 | } 25 | } 26 | 27 | resource brokerTagsAssignmentRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 28 | name: guid(resourceGroup().id, brokerTagsAssignment.name, 'broker-standard-tags-contributor') 29 | properties: { 30 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4a9ae827-6dc8-4573-8ac7-8239d42aa03f') // Tag Contributor 31 | principalId: userAssignedIdentity.properties.principalId 32 | principalType: 'ServicePrincipal' 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Altinn.Broker.Persistence.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Controllers/HealthController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | using Npgsql; 4 | 5 | namespace Altinn.Broker.Controllers; 6 | 7 | [ApiController] 8 | [Route("health")] 9 | [ApiExplorerSettings(IgnoreApi = true)] 10 | public class HealthController(NpgsqlDataSource databaseConnectionProvider) : ControllerBase 11 | { 12 | [HttpGet] 13 | public async Task HealthCheckAsync() 14 | { 15 | try 16 | { 17 | using var command = databaseConnectionProvider.CreateCommand("SELECT COUNT(*) FROM broker.file_transfer_status_description"); 18 | var count = (long)(await command.ExecuteScalarAsync() ?? 0); 19 | if (count == 0) 20 | { 21 | Console.Error.WriteLine("Health: Unable to query database. Is DatabaseOptions__ConnectionString set and is the database migrated"); 22 | return BadRequest("Unable to query database. Is DatabaseOptions__ConnectionString set and is the database migrated?"); 23 | } 24 | } 25 | catch (Exception e) 26 | { 27 | Console.Error.WriteLine("Health: Exception thrown while trying to query database: {exception}", e); 28 | return BadRequest("Exception thrown while trying to query database"); 29 | } 30 | 31 | return Ok("Environment properly configured"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/LoadTesting.md: -------------------------------------------------------------------------------- 1 | ## Load testing with k6 2 | Before running tests you should mock external dependencies like: 3 | - AltinnAuthorization by setting the function CheckUserAccess to return true 4 | - AltinnRegisterService to return a string 5 | - AltinnResourceRegister to return a ResourceEntity 6 | - Use the ConsoleLogEventBus instead of AltinnEventBus 7 | 8 | Constants: 9 | - BASE_URL; enviroment to test. 10 | - TOKENS: tokens for a service owner(TOKENS.DUMMY_SERVICE_OWNER_TOKEN) and a sender(TOKENS.DUMMY_SENDER_TOKEN), which can be found in postman(Authenticate as Sender/serviceOwner) in the Authenticator folder. 11 | 12 | k6 option variables: 13 | - VUs: How many virtual users running tests at the same time. 14 | - iterations: how many tests TOTAL should be completed. vus/iterations=test per vus. 0 means infinite iterations for as long as the test will run. 15 | - httpDebug: full/summary. Outputs infomration about http requests and responses 16 | - duration: How long the test should be running. The test also adds a 30 seconds graceful stop period on top of this. 17 | 18 | We run load tests using k6. To run without installing k6 you can use docker compose(base url has to be http://host.docker.internal:5096): 19 | ```docker compose -f docker-compose-loadtest.yml up k6-test``` 20 | 21 | if you have k6 installed locally, you can run it by using the following command: 22 | ```"k6 run test.js"``` 23 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Helpers/LogContextHelpers.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain; 2 | using Altinn.Broker.Models; 3 | 4 | using Serilog.Context; 5 | 6 | namespace Altinn.Broker.Helpers; 7 | 8 | public static class LogContextHelpers 9 | { 10 | public static void EnrichLogsWithInitializeFile(FileTransferInitalizeExt fileTransferInitalizeExt) 11 | { 12 | LogContext.PushProperty("sender", fileTransferInitalizeExt.Sender); 13 | LogContext.PushProperty("fileName", fileTransferInitalizeExt.FileName); 14 | LogContext.PushProperty("recipients", string.Join(',', fileTransferInitalizeExt.Recipients)); 15 | LogContext.PushProperty("sendersFileTransferReference", fileTransferInitalizeExt.SendersFileTransferReference); 16 | LogContext.PushProperty("checksum", fileTransferInitalizeExt.Checksum); 17 | } 18 | 19 | public static void EnrichLogsWithLegacyInitializeFile(LegacyFileInitalizeExt fileInitalizeExt) 20 | { 21 | LogContext.PushProperty("sender", fileInitalizeExt.Sender); 22 | LogContext.PushProperty("fileName", fileInitalizeExt.FileName); 23 | LogContext.PushProperty("recipients", string.Join(',', fileInitalizeExt.Recipients)); 24 | LogContext.PushProperty("sendersFileTransferReference", fileInitalizeExt.SendersFileTransferReference); 25 | LogContext.PushProperty("checksum", fileInitalizeExt.Checksum); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Repositories/IResourceRepository.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain; 2 | 3 | namespace Altinn.Broker.Core.Repositories; 4 | public interface IResourceRepository 5 | { 6 | Task GetResource(string resourceId, CancellationToken cancellationToken = default); 7 | Task UpdateMaxFileTransferSize(string resourceId, long maxSize, CancellationToken cancellationToken = default); 8 | Task CreateResource(ResourceEntity resource, CancellationToken cancellationToken = default); 9 | Task UpdateFileRetention(string resourceId, TimeSpan fileTransferTimeToLive, CancellationToken cancellationToken = default); 10 | Task UpdatePurgeFileTransferAfterAllRecipientsConfirmed(string resourceId, bool PurgeFileTransferAfterAllRecipientsConfirmed, CancellationToken cancellationToken = default); 11 | Task UpdatePurgeFileTransferGracePeriod(string resourceId, TimeSpan PurgeFileTransferGracePeriod, CancellationToken cancellationToken = default); 12 | Task UpdateUseManifestFileShim(string resourceId, bool useManifestFileShim, CancellationToken cancellationToken = default); 13 | Task UpdateExternalServiceCodeLegacy(string resourceId, string externalServiceCodeLegacy, CancellationToken cancellationToken = default); 14 | Task UpdateExternalServiceEditionCodeLegacy(string resourceId, int? externalServiceEditionCodeLegacy, CancellationToken cancellationToken = default); 15 | } 16 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.Tests/Data/R__Prepare_Test_Data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO broker.service_owner (service_owner_id_pk, service_owner_name) 2 | VALUES ('0192:991825827', 'Digitaliseringsdirektoratet Avd Oslo') 3 | ON CONFLICT (service_owner_id_pk) DO NOTHING; 4 | 5 | INSERT INTO broker.storage_provider (storage_provider_id_pk, service_owner_id_fk, created, storage_provider_type, resource_name, active) 6 | VALUES (DEFAULT, '0192:991825827', NOW(), 'Altinn3Azure', 'dummy-value', true) 7 | ON CONFLICT (service_owner_id_fk, storage_provider_type, active) DO NOTHING; 8 | 9 | INSERT INTO broker.altinn_resource ( 10 | resource_id_pk, 11 | created, 12 | max_file_transfer_size, 13 | file_transfer_time_to_live, 14 | organization_number, 15 | service_owner_id_fk 16 | ) VALUES ( 17 | 'altinn-broker-test-resource-1', 18 | NOW(), 19 | null, 20 | null, 21 | '991825827', 22 | '0192:991825827' 23 | ) 24 | ON CONFLICT (resource_id_pk) DO NOTHING; 25 | 26 | INSERT INTO broker.altinn_resource ( 27 | resource_id_pk, 28 | created, 29 | max_file_transfer_size, 30 | file_transfer_time_to_live, 31 | organization_number, 32 | service_owner_id_fk, 33 | use_manifest_file_shim 34 | ) VALUES ( 35 | 'manifest-shim-resource', 36 | NOW(), 37 | null, 38 | null, 39 | '991825827', 40 | '0192:991825827', 41 | true 42 | ) 43 | ON CONFLICT (resource_id_pk) DO NOTHING; -------------------------------------------------------------------------------- /src/Altinn.Broker.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "DatabaseOptions": { 9 | "ConnectionString": "Host=localhost:5432;Username=postgres;Password=postgres;Database=broker" 10 | }, 11 | "AzureResourceManagerOptions": { 12 | "Location": "norwayeast", 13 | "Environment": "test", 14 | "ClientId": "", 15 | "TenantId": "", 16 | "ClientSecret": "", 17 | "SubscriptionId": "" 18 | }, 19 | "AltinnOptions": { 20 | "OpenIdWellKnown": "https://platform.tt02.altinn.no/authentication/api/v1/openid/.well-known/openid-configuration", 21 | "LegacyOpenIdWellKnown": "https://test.maskinporten.no/.well-known/oauth-authorization-server", 22 | "PlatformGatewayUrl": "https://platform.tt02.altinn.no/", 23 | "PlatformSubscriptionKey": "" 24 | }, 25 | "MaskinportenSettings": { 26 | "Environment": "test", 27 | "ClientId": "", 28 | "Scope": "altinn:events.publish altinn:events.publish.admin altinn:register/partylookup.admin altinn:authorization/authorize.admin", 29 | "EncodedJwk": "", 30 | "ExhangeToAltinnToken": true 31 | }, 32 | "GeneralSettings": { 33 | "SlackUrl": "" 34 | }, 35 | "AzureStorageOptions": { 36 | "BlockSize": 33554432, 37 | "ConcurrentUploadThreads": 3, 38 | "BlocksBeforeCommit": 1000 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.Tests/ServiceOwnerControllerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using System.Text.Json; 3 | 4 | using Altinn.Broker.Models.ServiceOwner; 5 | using Altinn.Broker.Tests.Helpers; 6 | 7 | using Xunit; 8 | 9 | namespace Altinn.Broker.Tests; 10 | public class ServiceOwnerControllerTests : IClassFixture 11 | { 12 | private readonly CustomWebApplicationFactory _factory; 13 | private readonly HttpClient _serviceOwnerClient; 14 | private readonly JsonSerializerOptions _responseSerializerOptions; 15 | 16 | public ServiceOwnerControllerTests(CustomWebApplicationFactory factory) 17 | { 18 | _factory = factory; 19 | _serviceOwnerClient = _factory.CreateClientWithAuthorization(TestConstants.DUMMY_SERVICE_OWNER_TOKEN); 20 | _responseSerializerOptions = new JsonSerializerOptions(new JsonSerializerOptions() 21 | { 22 | PropertyNameCaseInsensitive = true 23 | }); 24 | _responseSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); 25 | } 26 | 27 | [Fact] 28 | public async Task Get_ServiceOwner() 29 | { 30 | var response = await _serviceOwnerClient.GetFromJsonAsync($"broker/api/v1/serviceowner", _responseSerializerOptions); 31 | Assert.Equal("Digitaliseringsdirektoratet Avd Oslo", response!.Name); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.Tests.LargeFile/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base 2 | USER app 3 | WORKDIR /app 4 | 5 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 6 | ARG BUILD_CONFIGURATION=Release 7 | WORKDIR /src 8 | COPY ["tests/Altinn.Broker.Tests.LargeFile/Altinn.Broker.Tests.LargeFile.csproj", "tests/Altinn.Broker.Tests.LargeFile/"] 9 | COPY ["src/Altinn.Broker.API/Altinn.Broker.API.csproj", "src/Altinn.Broker.API/"] 10 | COPY ["src/Altinn.Broker.Application/Altinn.Broker.Application.csproj", "src/Altinn.Broker.Application/"] 11 | COPY ["src/Altinn.Broker.Core/Altinn.Broker.Core.csproj", "src/Altinn.Broker.Core/"] 12 | COPY ["src/Altinn.Broker.Integrations/Altinn.Broker.Integrations.csproj", "src/Altinn.Broker.Integrations/"] 13 | COPY ["src/Altinn.Broker.Persistence/Altinn.Broker.Persistence.csproj", "src/Altinn.Broker.Persistence/"] 14 | RUN dotnet restore "./tests/Altinn.Broker.Tests.LargeFile/Altinn.Broker.Tests.LargeFile.csproj" 15 | COPY . . 16 | WORKDIR "/src/tests/Altinn.Broker.Tests.LargeFile" 17 | RUN dotnet build "./Altinn.Broker.Tests.LargeFile.csproj" -c $BUILD_CONFIGURATION -o /app/build 18 | 19 | FROM build AS publish 20 | ARG BUILD_CONFIGURATION=Release 21 | RUN dotnet publish "./Altinn.Broker.Tests.LargeFile.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false 22 | 23 | FROM base AS final 24 | WORKDIR /app 25 | COPY --from=publish /app/publish . 26 | ENTRYPOINT ["dotnet", "Altinn.Broker.Tests.LargeFile.dll"] -------------------------------------------------------------------------------- /.azure/modules/migrationJob/main.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param name string 3 | param image string 4 | param containerAppEnvId string 5 | param command string[] 6 | param environmentVariables { name: string, value: string?, secretRef: string? }[] = [] 7 | param secrets { name: string, keyVaultUrl: string, identity: string }[] = [] 8 | param volumes { name: string, storageName: string, storageType: string, mountOptions: string }[] = [] 9 | param volumeMounts { mountPath: string, subPath: string, volumeName: string }[] = [] 10 | param principalId string 11 | 12 | resource job 'Microsoft.App/jobs@2024-03-01' = { 13 | name: name 14 | location: location 15 | identity: { 16 | type: 'UserAssigned' 17 | userAssignedIdentities: { 18 | '${principalId}': {} 19 | } 20 | } 21 | properties: { 22 | configuration: { 23 | secrets: secrets 24 | manualTriggerConfig: { 25 | parallelism: 1 26 | replicaCompletionCount: 1 27 | } 28 | replicaRetryLimit: 1 29 | replicaTimeout: 120 30 | triggerType: 'Manual' 31 | } 32 | environmentId: containerAppEnvId 33 | template: { 34 | containers: [ 35 | { 36 | env: environmentVariables 37 | image: image 38 | name: name 39 | command: command 40 | volumeMounts: volumeMounts 41 | } 42 | ] 43 | volumes: volumes 44 | } 45 | } 46 | } 47 | output name string = job.name 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/chore.yml: -------------------------------------------------------------------------------- 1 | name: Chore ✅ 2 | description: Create a none user-story issue (chore, tech issue, backend issue) 3 | labels: ["kind/chore", "status/draft"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Please make sure this chore hasn't been already submitted by someone by looking through other open/closed chore issues. 9 | 10 | - type: textarea 11 | id: description 12 | attributes: 13 | label: Description 14 | description: Give us a brief description of the work that needs to be done. 15 | validations: 16 | required: true 17 | 18 | - type: textarea 19 | id: additional-information 20 | attributes: 21 | label: Additional Information 22 | description: Add more details as need like links, architecture sketches etc. 23 | 24 | - type: textarea 25 | id: tasks 26 | attributes: 27 | label: Tasks 28 | description: Add tasks to be done as part of this issue. 29 | 30 | - type: textarea 31 | id: acceptance-criterias 32 | attributes: 33 | label: Acceptance Criterias 34 | description: Define the acceptance criterias that this user story should testet against (if relevant). 35 | 36 | - type: markdown 37 | attributes: 38 | value: | 39 | * Check the [Definition of Ready](https://docs.altinn.studio/community/devops/definition-of-ready/) if you need hints on what to include. 40 | * Remember to add the correct labels (status/*, team/*, org/*) 41 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Integrations/Hangfire/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | using Hangfire; 2 | using Hangfire.PostgreSql; 3 | 4 | using Microsoft.ApplicationInsights; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Logging; 7 | using Newtonsoft.Json; 8 | using Altinn.Broker.Integrations.Slack; 9 | using Hangfire.AspNetCore; 10 | 11 | namespace Altinn.Broker.Integrations.Hangfire; 12 | public static class DependencyInjection 13 | { 14 | public static void ConfigureHangfire(this IServiceCollection services) 15 | { 16 | services.AddSingleton(); 17 | services.AddHangfire((provider, config) => 18 | { 19 | config.UsePostgreSqlStorage( 20 | c => c.UseConnectionFactory(provider.GetRequiredService()) 21 | ); 22 | config.UseLogProvider(new AspNetCoreLogProvider(provider.GetRequiredService())); 23 | config.UseFilter(new HangfireAppRequestFilter()); 24 | config.UseSerializerSettings(new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }); 25 | config.UseFilter( 26 | new SlackExceptionHandler( 27 | provider.GetRequiredService(), 28 | provider.GetRequiredService>()) 29 | ); 30 | } 31 | ); 32 | services.AddHangfireServer(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.bruno/SystemRegister/Register system.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Register system 3 | type: http 4 | seq: 1 5 | } 6 | 7 | post { 8 | url: {{platform_base_url}}/authentication/api/v1/systemregister/vendor 9 | body: json 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: {{systemprovider_token}} 15 | } 16 | 17 | body:json { 18 | { 19 | "id": "{{serviceowner_orgnumber}}_{{resource_id}}_broker", 20 | "systemVendorOrgNumber": "{{serviceowner_orgnumber}}", 21 | "vendor": { 22 | "authority": "iso6523-actorid-upis", 23 | "ID": "0192:{{serviceowner_orgnumber}}" 24 | }, 25 | "name": { 26 | "en": "BrokerTestSystem-{{serviceowner_orgnumber}}_{{resource_id}}", 27 | "nb": "BrokerTestSystem-{{serviceowner_orgnumber}}_{{resource_id}}", 28 | "nn": "BrokerTestSystem-{{serviceowner_orgnumber}}_{{resource_id}}" 29 | }, 30 | "description": { 31 | "en": "From Bruno collection", 32 | "nb": "Fra Bruno collection", 33 | "nn": "Fra Bruno collection" 34 | }, 35 | "rights": [ 36 | { 37 | "resource": [ 38 | { 39 | "value": "{{resource_id}}", 40 | "id": "urn:altinn:resource" 41 | } 42 | ] 43 | } 44 | ], 45 | "allowedRedirectUrls": [ 46 | "https://smartcloudaltinn.azurewebsites.net/receipt" 47 | ], 48 | "clientId": [ 49 | "{{client_id}}" 50 | ] 51 | } 52 | } 53 | 54 | script:post-response { 55 | const responseData = res.body; 56 | bru.setVar("system_id", responseData); 57 | } 58 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Common/Constants/UrnConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Common.Constants; 2 | public static class UrnConstants 3 | { 4 | /// 5 | /// xacml string that represents authentication level 6 | /// 7 | public const string AuthenticationLevel = "urn:altinn:authlevel"; 8 | /// 9 | /// xacml string that represents minimum authentication level 10 | /// 11 | public const string MinimumAuthenticationLevel = "urn:altinn:minimum-authenticationlevel"; 12 | /// 13 | /// xacml string that represents organization number 14 | /// 15 | public const string OrganizationNumberAttribute = "urn:altinn:organization:identifier-no"; 16 | /// 17 | /// xacml string that represents party 18 | /// 19 | public const string Party = "urn:altinn:partyid"; 20 | /// 21 | /// xacml string that represents person identifier 22 | /// 23 | public const string PersonIdAttribute = "urn:altinn:person:identifier-no"; 24 | /// 25 | /// xacml string that represents resource 26 | /// 27 | public const string Resource = "urn:altinn:resource"; 28 | /// 29 | /// xacml string that represents resource instance 30 | /// 31 | public const string ResourceInstance = "urn:altinn:resourceinstance"; 32 | /// 33 | /// xacml string that represents session id 34 | /// 35 | public const string SessionId = "urn:altinn:sessionid"; 36 | } 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/analysis.yml: -------------------------------------------------------------------------------- 1 | name: Analysis 🔬 2 | description: Create a new analysis issue for something big that needs some insight. 3 | labels: ["kind/analysis", "status/draft"] 4 | body: 5 | - type: textarea 6 | id: description 7 | attributes: 8 | label: Description 9 | description: Description of the the problem or area that is to be analysed. 10 | validations: 11 | required: true 12 | 13 | - type: textarea 14 | id: in-scope 15 | attributes: 16 | label: In scope 17 | description: What's in scope for this analysis? 18 | 19 | - type: textarea 20 | id: out-of-scope 21 | attributes: 22 | label: Out of scope 23 | description: What's out of scope for this analysis? 24 | 25 | - type: textarea 26 | id: additional-information 27 | attributes: 28 | label: Additional Information 29 | description: Links to relevant resources, documentation of other issues, UX sketches, technical architecture/requirements. 30 | 31 | - type: textarea 32 | id: analysis 33 | attributes: 34 | label: Analysis 35 | description: A description of how the analysis was done, what alternatives were considered etc 36 | 37 | - type: textarea 38 | id: conclusion 39 | attributes: 40 | label: Conclusion 41 | description: What have we found out by doing this analysis 42 | 43 | - type: markdown 44 | attributes: 45 | value: | 46 | * Remember to add correct labels. 47 | * Remember to link all relevant issues (bugs, user stories, chores). 48 | -------------------------------------------------------------------------------- /.github/workflows/test-application.yml: -------------------------------------------------------------------------------- 1 | name: Test application 2 | 3 | on: 4 | workflow_call: 5 | workflow_dispatch: 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test-application: 11 | name: Test application 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v4 18 | with: 19 | dotnet-version: | 20 | 9.0.x 21 | 22 | - name: Start dependencies for tests (docker compose) 23 | run: | 24 | docker compose up -d & # Run in background and disown the process 25 | disown 26 | 27 | - name: Restore 28 | run: dotnet restore 29 | 30 | - name: Build 31 | run: dotnet build --no-restore --configuration Release 32 | 33 | - name: Wait for docker compose services to be ready and database migration to complete 34 | run: | 35 | timeout 5m bash -c ' 36 | while ! docker ps | grep -q "(healthy)"; do 37 | echo "Waiting for services to be healthy..." 38 | sleep 5 39 | done 40 | while docker ps -a | grep -q "database_migration"; do 41 | if docker ps -a | grep -q "Exited.*database_migration"; then 42 | echo "Database migration completed." 43 | break 44 | fi 45 | echo "Waiting for database migration to complete..." 46 | sleep 5 47 | done 48 | ' 49 | - name: Test 50 | run: dotnet test --no-build --configuration Release 51 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/GenerateReport/GenerateDailySummaryReportResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Application.GenerateReport; 2 | 3 | public class GenerateDailySummaryReportResponse 4 | { 5 | /// 6 | /// URL to the generated parquet file in blob storage. 7 | /// 8 | public required string FilePath { get; set; } 9 | 10 | /// 11 | /// MD5 hash of the generated file. 12 | /// 13 | public required string FileHash { get; set; } 14 | 15 | /// 16 | /// Size of the generated file in bytes. 17 | /// 18 | public required long FileSizeBytes { get; set; } 19 | 20 | /// 21 | /// Number of unique service owners included in the report. 22 | /// 23 | public required int ServiceOwnerCount { get; set; } 24 | 25 | /// 26 | /// Total number of file transfers included in the report. 27 | /// 28 | public required int TotalFileTransferCount { get; set; } 29 | 30 | /// 31 | /// Timestamp when the report was generated. 32 | /// 33 | public required DateTimeOffset GeneratedAt { get; set; } 34 | 35 | /// 36 | /// The environment the report was generated for. 37 | /// 38 | public required string Environment { get; set; } 39 | 40 | /// 41 | /// Indicates if Altinn2 file transfers were included in the report. 42 | /// Note: Broker only supports Altinn3, so this will always be false. 43 | /// 44 | public required bool Altinn2Included { get; set; } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.UseCaseTests/helpers/brokerPayloadBuilder.js: -------------------------------------------------------------------------------- 1 | import { toUrn } from './commonUtils.js'; 2 | 3 | export const TEST_TAG_A3 = 'useCaseTestsA3'; 4 | export const TEST_TAG_LEGACY = 'useCaseTestsLegacy'; 5 | const sender = __ENV.sender; 6 | 7 | export function buildInitializeFileTransferPayload(recipientOrgNo) { 8 | const recipient = toUrn(recipientOrgNo); 9 | const nowRef = `usecase-broker-${Date.now()}`; 10 | 11 | return { 12 | resourceId: "bruksmonster-broker", 13 | fileName: 'usecase-broker-test-file.txt', 14 | sendersFileTransferReference: nowRef, 15 | sender: `0192:${sender}`, 16 | recipients: [recipient], 17 | propertyList: { 18 | testTag: TEST_TAG_A3, 19 | useCase: 'Use case tests', 20 | description: 'Test file transfer initialization for use case tests' 21 | }, 22 | disableVirusScan: false 23 | }; 24 | } 25 | 26 | export function buildLegacyInitializeFileTransferPayload(recipientOrgNo) { 27 | const recipient = "0192:" + recipientOrgNo; 28 | const nowRef = `legacy-usecase-broker-${Date.now()}`; 29 | return { 30 | resourceId: "bruksmonster-broker", 31 | fileName: 'usecase-broker-test-file.txt', 32 | sendersFileTransferReference: nowRef, 33 | sender:`0192:${sender}`, 34 | recipients: [recipient], 35 | propertyList: { 36 | testTag: TEST_TAG_LEGACY, 37 | useCase: 'Use case tests', 38 | description: 'Test file transfer initialization for legacy use case tests' 39 | }, 40 | disableVirusScan: false 41 | }; 42 | } -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Helpers/AltinnTokenEventsHelper.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.API.Models.Maskinporten; 2 | 3 | using Microsoft.AspNetCore.Authentication.JwtBearer; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.IdentityModel.Tokens; 6 | 7 | namespace Altinn.Broker.API.Helpers; 8 | 9 | public class AltinnTokenEventsHelper 10 | { 11 | public static Task OnAuthenticationFailed(AuthenticationFailedContext context) 12 | { 13 | // Allow authentication failures to be handled by the default mechanism 14 | // This removes the blocking of Maskinporten tokens to support pure Maskinporten tokens 15 | return Task.CompletedTask; 16 | } 17 | 18 | public static async Task OnChallenge(JwtBearerChallengeContext context) 19 | { 20 | if (context.AuthenticateFailure != null && context.AuthenticateFailure is MaskinportenSecurityTokenException) 21 | { 22 | context.HandleResponse(); 23 | context.Response.Headers.Append("WWW-Authenticate", context.Options.Challenge + " error=\"invalid_token\""); 24 | context.Response.ContentType = "application/problem+json"; 25 | context.Response.StatusCode = StatusCodes.Status403Forbidden; 26 | await context.Response.WriteAsJsonAsync(new ProblemDetails() 27 | { 28 | Status = StatusCodes.Status403Forbidden, 29 | Title = "IDX10205: Issuer validation failed", 30 | Detail = "Maskinporten token is not valid. Exchange to Altinn token and try again. Read more at https://docs.altinn.studio/api/scenarios/authentication/#maskinporten-jwt-access-token-input" 31 | }); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/GenerateReport/GenerateAndDownloadDailySummaryReportResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Application.GenerateReport; 2 | 3 | public class GenerateAndDownloadDailySummaryReportResponse 4 | { 5 | /// 6 | /// The parquet file stream 7 | /// 8 | public required Stream FileStream { get; set; } 9 | 10 | /// 11 | /// The filename for the download 12 | /// 13 | public required string FileName { get; set; } 14 | 15 | /// 16 | /// MD5 hash of the file 17 | /// 18 | public required string FileHash { get; set; } 19 | 20 | /// 21 | /// File size in bytes 22 | /// 23 | public long FileSizeBytes { get; set; } 24 | 25 | /// 26 | /// Number of service owners included in the report 27 | /// 28 | public int ServiceOwnerCount { get; set; } 29 | 30 | /// 31 | /// Total number of file transfers included in the report 32 | /// 33 | public int TotalFileTransferCount { get; set; } 34 | 35 | /// 36 | /// When the report was generated (UTC) 37 | /// 38 | public DateTimeOffset GeneratedAt { get; set; } 39 | 40 | /// 41 | /// Environment where the report was generated 42 | /// 43 | public required string Environment { get; set; } 44 | 45 | /// 46 | /// Whether Altinn2 file transfers were included 47 | /// Note: Broker only supports Altinn3, so this will always be false. 48 | /// 49 | public bool Altinn2Included { get; set; } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/user_story.yml: -------------------------------------------------------------------------------- 1 | name: User Story 😃 2 | description: Create a new user story 3 | labels: ["kind/user-story", "status/draft"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | * Please make sure this user story hasn't been already submitted by someone by looking through other open/closed user stories. 9 | * Consider starting with the format As a **[who]** I want to **[what]** so that **[why]** 10 | * Consider the [INVEST](https://www.pivotaltracker.com/blog/how-to-invest-in-your-user-stories) qualities when writing the story 11 | 12 | - type: textarea 13 | id: description 14 | attributes: 15 | label: Description 16 | description: Give us a brief description of the feature or enhancement you would like 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: additional-information 22 | attributes: 23 | label: Additional Information 24 | description: Add more details as needed, like links, screenshots, etc. 25 | 26 | - type: textarea 27 | id: tasks 28 | attributes: 29 | label: Tasks 30 | description: Add tasks to be done as part of this story. 31 | 32 | - type: textarea 33 | id: acceptance-criterias 34 | attributes: 35 | label: Acceptance Criterias 36 | description: Define the acceptance criterias that this user story should testet against 37 | 38 | - type: markdown 39 | attributes: 40 | value: | 41 | * Check the [Definition of Ready](https://docs.altinn.studio/community/devops/definition-of-ready/) if you need hints on what to include. 42 | * Remember to add the correct labels (status/*, org/*, ...) 43 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/ValidationAttributes/PropertyListAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Altinn.Broker.API.ValidationAttributes 4 | { 5 | [AttributeUsage(AttributeTargets.Property)] 6 | internal class PropertyListAttribute : ValidationAttribute 7 | { 8 | public PropertyListAttribute() 9 | { 10 | } 11 | 12 | protected override ValidationResult IsValid(object? value, ValidationContext validationContext) 13 | { 14 | if (value == null) 15 | { 16 | return ValidationResult.Success!; 17 | } 18 | 19 | if (!(value is Dictionary)) 20 | { 21 | return new ValidationResult("PropertyList Object is not of proper type"); 22 | } 23 | 24 | var dictionary = (Dictionary)value; 25 | 26 | if (dictionary.Count > 10) 27 | return new ValidationResult("PropertyList can contain at most 10 properties"); 28 | 29 | foreach (var keyValuePair in dictionary) 30 | { 31 | if (keyValuePair.Key.Length > 50) 32 | return new ValidationResult(String.Format("PropertyList Key can not be longer than 50. Length:{0}, KeyValue:{1}", keyValuePair.Key.Length.ToString(), keyValuePair.Key)); 33 | 34 | if (keyValuePair.Value.Length > 300) 35 | return new ValidationResult(String.Format("PropertyList Value can not be longer than 300. Length:{0}, Value:{1}", keyValuePair.Value.Length.ToString(), keyValuePair.Value)); 36 | } 37 | 38 | return ValidationResult.Success!; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /.github/tools/revisionVerifier.sh: -------------------------------------------------------------------------------- 1 | if [ -z "$1" ]; then 2 | echo "Usage: $0 " 3 | exit 1 4 | fi 5 | 6 | if [ -z "$2" ]; then 7 | echo "Usage: $0 " 8 | exit 1 9 | fi 10 | 11 | revision_name="$1" 12 | resource_group="$2" 13 | query_filter="{name:name, runningState:properties.runningState, healthState:properties.healthState}" 14 | 15 | verify_revision() { 16 | local json_output 17 | 18 | # Fetch app revision 19 | json_output=$(az containerapp revision show -g "$resource_group" --revision "$revision_name" --query "$query_filter" 2>/dev/null) 20 | 21 | health_state=$(echo $json_output | jq -r '.healthState') 22 | running_state=$(echo $json_output | jq -r '.runningState') 23 | 24 | echo "Revision $revision_name status:" 25 | echo "-----------------------------" 26 | echo "Health state: $health_state" 27 | echo "Running state: $running_state" 28 | echo " " 29 | 30 | # Check health and running status 31 | if [[ $health_state == "Healthy" && ($running_state == "Running" || $running_state == "RunningAtMaxScale") ]]; then 32 | return 0 # OK! 33 | else 34 | if [[ $running_state == "Failed" ]]; then 35 | echo "Revision $revision_name failed. Exiting script." 36 | exit 1 37 | fi 38 | return 1 # Not OK! 39 | fi 40 | } 41 | 42 | attempt=1 43 | 44 | while true; do 45 | if verify_revision; then 46 | echo "Revision $revision_name is healthy and running" 47 | break 48 | else 49 | echo "Attempt $attempt: Waiting for revision $revision_name ..." 50 | sleep 10 # Sleep for 10 seconds 51 | attempt=$((attempt+1)) 52 | if [[ $attempt -gt 25 ]]; then 53 | echo "Revision $revision_name did not start in time. Exiting script." 54 | exit 1 55 | fi 56 | fi 57 | done -------------------------------------------------------------------------------- /tests/Altinn.Broker.UseCaseTests/helpers/maskinportenTokenService.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http'; 2 | import { buildMaskinportenJwt } from './maskinportenJwtBuilder.js'; 3 | 4 | const maskinportenUrl = (__ENV.base_url.toLowerCase().includes('platform.altinn.no')) 5 | ? 'https://maskinporten.no/token' 6 | : 'https://test.maskinporten.no/token'; 7 | 8 | export async function retrieveMaskinportenToken({ clientId, kid, pem, scope, isSender }) { 9 | const jwt = await buildMaskinportenJwt({ clientId, kid, pem, scope, tokenUrl: maskinportenUrl , isSender}); 10 | const url = maskinportenUrl; 11 | const headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' }; 12 | const body = Object.entries({ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: jwt }) 13 | .map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v)).join('&'); 14 | const res = http.post(url, body, { headers }); 15 | if (res.status !== 200) { 16 | throw new Error(`Maskinporten token failed: status=${res.status} body=${res.body}`); 17 | } 18 | return res.json('access_token'); 19 | } 20 | 21 | export async function getMaintenanceMaskinportenToken() { 22 | return await retrieveMaskinportenToken({ 23 | clientId: __ENV.mp_client_id, 24 | kid: __ENV.mp_kid, 25 | pem: __ENV.mp_client_pem, 26 | scope: 'altinn:broker.maintenance', 27 | isSender: false 28 | }); 29 | } 30 | 31 | export async function getLegacyMaskinportenToken() { 32 | return await retrieveMaskinportenToken({ 33 | clientId: __ENV.mp_client_id, 34 | kid: __ENV.mp_kid, 35 | pem: __ENV.mp_client_pem, 36 | scope: 'altinn:broker.legacy', 37 | isSender: false 38 | }); 39 | } -------------------------------------------------------------------------------- /.github/tools/pwdGenerator.ps1: -------------------------------------------------------------------------------- 1 | function Get-RandomCharacters([int]$length, [string]$characters) { 2 | $random = 1..$length | ForEach-Object { Get-Random -Maximum $characters.length } 3 | $private:ofs="" 4 | return [string]$characters[$random] 5 | } 6 | function Scramble-String([string]$inputString){ 7 | $characterArray = $inputString.ToCharArray() 8 | $scrambledStringArray = $characterArray | Get-Random -Count $characterArray.Length 9 | $outputString = -join $scrambledStringArray 10 | return $outputString 11 | } 12 | function GeneratePassword{ 13 | param( 14 | [Parameter()] 15 | [ValidateRange(8,64)] 16 | [int]$length=25, 17 | [Parameter()] 18 | [ValidateRange(0,64)] 19 | [int]$minLower=1, 20 | [Parameter()] 21 | [ValidateRange(0,64)] 22 | [int]$minUpper=1, 23 | [Parameter()] 24 | [ValidateRange(0,64)] 25 | [int]$minNumber=1, 26 | [Parameter()] 27 | [ValidateRange(0,64)] 28 | [int]$minSpecial=1 29 | ) 30 | $lowercase = 'abcdefghiklmnoprstuvwxyz' 31 | $uppercase = 'ABCDEFGHKLMNOPRSTUVWXYZ' 32 | $numbers = '1234567890' 33 | $special = '~!@#^()_-' 34 | $characters = $lowercase + $uppercase + $numbers + $special 35 | $password = Get-RandomCharacters $minLower $lowercase 36 | $password += Get-RandomCharacters $minUpper $uppercase 37 | $password += Get-RandomCharacters $minNumber $numbers 38 | $password += Get-RandomCharacters $minSpecial $special 39 | $password += Get-RandomCharacters $($length-$password.Length) $characters 40 | $password = Scramble-String $password 41 | $Bytes = [System.Text.Encoding]::Unicode.GetBytes($password) 42 | $EncodedText =[Convert]::ToBase64String($Bytes) 43 | return @{ 44 | Password = $password 45 | EncodedPassword = $EncodedText 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Models/ServiceOwner/ServiceOwnerOverviewExt.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Broker.Models.ServiceOwner; 2 | 3 | /// 4 | /// Represents the Broker properties of a service owner. 5 | /// 6 | public class ServiceOwnerOverviewExt 7 | { 8 | public ServiceOwnerOverviewExt() { } 9 | 10 | /// 11 | /// The name of the service owner. 12 | /// 13 | public required string Name { get; set; } 14 | 15 | /// 16 | /// The service owner's storage providers. 17 | /// 18 | public required List StorageProviders { get; set; } = new List(); 19 | } 20 | 21 | 22 | /// 23 | /// Represents the Broker properties of a storage provider. 24 | /// 25 | public class StorageProviderExt 26 | { 27 | /// 28 | /// The Storage provider type. 29 | /// 30 | public required StorageProviderTypeExt Type { get; set; } 31 | 32 | /// 33 | /// The deployment status of the storage provider. 34 | /// 35 | public required DeploymentStatusExt DeploymentStatus { get; set; } 36 | 37 | /// 38 | /// The deployment environment of the storage provider. 39 | /// 40 | public required string DeploymentEnvironment { get; set; } 41 | } 42 | 43 | /// 44 | /// Represents the storage provider type. 45 | /// 46 | public enum StorageProviderTypeExt 47 | { 48 | /// 49 | /// Azure storage provider which scans files for viruses. 50 | /// 51 | AltinnAzure = 0, 52 | 53 | /// 54 | /// Azure storage provider which does not scan files for viruses. 55 | /// 56 | AltinnAzureWithoutVirusScan = 1, 57 | } 58 | -------------------------------------------------------------------------------- /.github/actions/remediate-standard-tags/action.yml: -------------------------------------------------------------------------------- 1 | name: Remediate standard tags 2 | 3 | description: "Run remediation for the broker standard tags policy" 4 | 5 | inputs: 6 | AZURE_CLIENT_ID: 7 | description: "Client ID for the service principal" 8 | required: true 9 | AZURE_TENANT_ID: 10 | description: "Tenant ID for the service principal" 11 | required: true 12 | AZURE_SUBSCRIPTION_ID: 13 | description: "Subscription ID for the service principal" 14 | required: true 15 | AZURE_NAME_PREFIX: 16 | description: "Prefix for all resources (resource group name prefix)" 17 | required: true 18 | environment: 19 | description: "Deployment environment name (e.g., test/staging/production)" 20 | required: true 21 | 22 | runs: 23 | using: "composite" 24 | steps: 25 | - name: OIDC Login to Azure Public Cloud 26 | uses: azure/login@v2 27 | with: 28 | client-id: ${{ inputs.AZURE_CLIENT_ID }} 29 | tenant-id: ${{ inputs.AZURE_TENANT_ID }} 30 | subscription-id: ${{ inputs.AZURE_SUBSCRIPTION_ID }} 31 | 32 | - name: Remediate standard tags policy 33 | shell: pwsh 34 | run: | 35 | $rgName = "${{ inputs.AZURE_NAME_PREFIX }}-rg" 36 | $subscriptionId = "${{ inputs.AZURE_SUBSCRIPTION_ID }}" 37 | $assignmentId = "/subscriptions/$subscriptionId/resourceGroups/$rgName/providers/Microsoft.Authorization/policyAssignments/broker-standard-tags" 38 | az policy remediation create ` 39 | --name "broker-standard-tags-remediation" ` 40 | --policy-assignment $assignmentId ` 41 | --resource-group $rgName 42 | 43 | - name: Logout from azure 44 | shell: bash 45 | if: ${{failure() || success()}} 46 | continue-on-error: true 47 | run: az logout 48 | 49 | 50 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.Tests/Factories/FileTransferEntityFactory.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Domain; 2 | using Altinn.Broker.Core.Domain.Enums; 3 | using Altinn.Broker.Tests.Helpers; 4 | 5 | namespace Altinn.Broker.Tests.Factories; 6 | internal static class FileTransferEntityFactory 7 | { 8 | internal static FileTransferEntity BasicFileTransfer() 9 | { 10 | var fileTransferId = Guid.NewGuid(); 11 | return new() 12 | { 13 | FileTransferId = fileTransferId, 14 | ResourceId = TestConstants.RESOURCE_FOR_TEST, 15 | Checksum = null, 16 | FileName = "input.txt", 17 | PropertyList = [], 18 | RecipientCurrentStatuses = new List 19 | { 20 | new ActorFileTransferStatusEntity 21 | { 22 | Actor = new ActorEntity() 23 | { 24 | ActorExternalId = "0192:986252932" 25 | }, 26 | Date = DateTime.UtcNow, 27 | FileTransferId = fileTransferId 28 | } 29 | }, 30 | Sender = new ActorEntity() 31 | { 32 | ActorExternalId = "0192:991825827" 33 | }, 34 | SendersFileTransferReference = "test-data", 35 | Created = DateTime.UtcNow, 36 | ExpirationTime = DateTime.UtcNow.AddHours(1), 37 | FileTransferStatusEntity = new FileTransferStatusEntity() 38 | { 39 | FileTransferId = fileTransferId, 40 | Date = DateTime.UtcNow, 41 | DetailedStatus = "Ready for download", 42 | Status = FileTransferStatus.Published 43 | } 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Swagger/BinaryRequestBodyOperationFilter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.OpenApi.Models; 3 | using Swashbuckle.AspNetCore.SwaggerGen; 4 | 5 | namespace Altinn.Broker.API.Swagger; 6 | 7 | /// 8 | /// Ensures endpoints that consume application/octet-stream expose a binary requestBody in OpenAPI. 9 | /// 10 | public sealed class BinaryRequestBodyOperationFilter : IOperationFilter 11 | { 12 | private const string OctetStream = "application/octet-stream"; 13 | 14 | public void Apply(OpenApiOperation operation, OperationFilterContext context) 15 | { 16 | var consumesMediaTypes = context.ApiDescription.SupportedRequestFormats? 17 | .Select(f => f.MediaType) 18 | .Where(m => !string.IsNullOrWhiteSpace(m)) 19 | .Select(m => m!.Trim().ToLowerInvariant()) 20 | .ToList() ?? []; 21 | 22 | var hasOctetStreamViaApiDescription = consumesMediaTypes.Contains(OctetStream); 23 | 24 | var hasOctetStreamViaConsumesAttribute = 25 | context.MethodInfo 26 | .GetCustomAttributes(true) 27 | .OfType() 28 | .Any(a => a.ContentTypes.Any(ct => string.Equals(ct, OctetStream, StringComparison.OrdinalIgnoreCase))); 29 | 30 | if (!hasOctetStreamViaApiDescription && !hasOctetStreamViaConsumesAttribute) return; 31 | 32 | operation.RequestBody ??= new OpenApiRequestBody 33 | { 34 | Required = true 35 | }; 36 | 37 | operation.RequestBody.Content[OctetStream] = new OpenApiMediaType 38 | { 39 | Schema = new OpenApiSchema 40 | { 41 | Type = "string", 42 | Format = "binary" 43 | } 44 | }; 45 | } 46 | } 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Repositories/IdempotencyEventRepository.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Core.Repositories; 2 | using Altinn.Broker.Persistence.Helpers; 3 | 4 | using Npgsql; 5 | 6 | namespace Altinn.Broker.Persistence.Repositories; 7 | 8 | public class IdempotencyEventRepository(NpgsqlDataSource dataSource, ExecuteDBCommandWithRetries commandExecutor) : IIdempotencyEventRepository 9 | { 10 | public async Task AddIdempotencyEventAsync(string IdempotencyEventId, CancellationToken cancellationToken) 11 | { 12 | await using NpgsqlCommand command = dataSource.CreateCommand( 13 | "INSERT INTO broker.idempotency_event (idempotency_event_id_pk, created)" + 14 | "VALUES (@idempotency_event_id_pk, @created) "); 15 | command.Parameters.AddWithValue("@idempotency_event_id_pk", IdempotencyEventId); 16 | command.Parameters.AddWithValue("@created", DateTime.UtcNow); 17 | 18 | await commandExecutor.ExecuteWithRetry(command.ExecuteNonQueryAsync, cancellationToken); 19 | } 20 | 21 | public async Task TryAddIdempotencyEventAsync(string IdempotencyEventId, CancellationToken cancellationToken) 22 | { 23 | await using NpgsqlCommand command = dataSource.CreateCommand( 24 | "INSERT INTO broker.idempotency_event (idempotency_event_id_pk, created) " + 25 | "VALUES (@idempotency_event_id_pk, @created) ON CONFLICT (idempotency_event_id_pk) DO NOTHING"); 26 | command.Parameters.AddWithValue("@idempotency_event_id_pk", IdempotencyEventId); 27 | command.Parameters.AddWithValue("@created", DateTime.UtcNow); 28 | 29 | var affected = await commandExecutor.ExecuteWithRetry(async (ct) => await command.ExecuteNonQueryAsync(ct), cancellationToken); 30 | return affected > 0; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Persistence/Migrations/V0018__denormalized_file_transfer_status.sql: -------------------------------------------------------------------------------- 1 | -- 1. Add latest file status columns to file_transfer 2 | ALTER TABLE broker.file_transfer 3 | ADD COLUMN IF NOT EXISTS latest_file_status_id int4, 4 | ADD COLUMN IF NOT EXISTS latest_file_status_date timestamp; 5 | 6 | -- 2. Create NEW table for latest actor statuses per file WITH indexes 7 | CREATE TABLE broker.actor_file_transfer_latest_status ( 8 | file_transfer_id_fk uuid NOT NULL, 9 | actor_id_fk int8 NOT NULL, 10 | latest_actor_status_id int4 NOT NULL, 11 | latest_actor_status_date timestamp NOT NULL, 12 | 13 | -- Primary key 14 | CONSTRAINT actor_file_transfer_latest_status_pkey 15 | PRIMARY KEY (file_transfer_id_fk, actor_id_fk), 16 | 17 | -- Foreign keys 18 | CONSTRAINT actor_file_transfer_latest_status_file_fk 19 | FOREIGN KEY (file_transfer_id_fk) 20 | REFERENCES broker.file_transfer(file_transfer_id_pk) 21 | ON DELETE CASCADE, 22 | CONSTRAINT actor_file_transfer_latest_status_actor_fk 23 | FOREIGN KEY (actor_id_fk) 24 | REFERENCES broker.actor(actor_id_pk) 25 | ON DELETE CASCADE, 26 | CONSTRAINT actor_file_transfer_latest_status_status_fk 27 | FOREIGN KEY (latest_actor_status_id) 28 | REFERENCES broker.actor_file_transfer_status_description(actor_file_transfer_status_description_id_pk) 29 | ); 30 | 31 | -- No need to do concurrently here as it is a new table 32 | CREATE INDEX idx_afls_actor_status 33 | ON broker.actor_file_transfer_latest_status (actor_id_fk, latest_actor_status_id, file_transfer_id_fk); 34 | 35 | CREATE INDEX idx_afls_file 36 | ON broker.actor_file_transfer_latest_status (file_transfer_id_fk); 37 | 38 | CREATE INDEX idx_afls_actor_file_status 39 | ON broker.actor_file_transfer_latest_status (actor_id_fk, file_transfer_id_fk, latest_actor_status_id); 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/epic.yml: -------------------------------------------------------------------------------- 1 | name: Epic 💎 2 | description: Create a new epic based on user and vendor views 3 | labels: ["kind/epic", "status/draft"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: "## Users View" 8 | 9 | - type: textarea 10 | id: users-view 11 | attributes: 12 | label: Title, User role(s), and Users value statement(s) 13 | description: Provide the title, user role(s), and value statement(s) from the user's perspective. 14 | validations: 15 | required: true 16 | 17 | - type: markdown 18 | attributes: 19 | value: "## Vendors View" 20 | 21 | - type: textarea 22 | id: vendors-view-description 23 | attributes: 24 | label: Description 25 | description: High level features (capabilities) and additional information from the vendor's perspective. 26 | 27 | - type: markdown 28 | attributes: 29 | value: "```[tasklist]\n### Features\n```" 30 | 31 | - type: markdown 32 | attributes: 33 | value: "```[tasklist]\n### Work items" 34 | 35 | - type: textarea 36 | id: work-items-tasklist 37 | attributes: 38 | label: List of Work Items 39 | description: Provide the list of work items to be considered for this epic. 40 | 41 | - type: markdown 42 | attributes: 43 | value: "## Item Attributes" 44 | 45 | - type: textarea 46 | id: item-attributes 47 | attributes: 48 | label: Automatically updated properties 49 | description: Provide the automatically updated properties related to this epic. 50 | 51 | - type: markdown 52 | attributes: 53 | value: | 54 | * Check the [Definition of Ready](https://docs.altinn.studio/community/devops/definition-of-ready/) if you need hints on what to include. 55 | * Remember to link all relevant issues (bugs, user stories, chores) 56 | -------------------------------------------------------------------------------- /.bruno/Authentication/Get Systemprovider Maskinporten Token.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Get systemprovider Maskinporten token 3 | type: http 4 | seq: 1 5 | } 6 | 7 | post { 8 | url: https://test.maskinporten.no/token 9 | body: formUrlEncoded 10 | auth: none 11 | } 12 | 13 | body:form-urlencoded { 14 | grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer 15 | assertion: {{jwt}} 16 | } 17 | 18 | script:pre-request { 19 | const crypto = require('crypto'); 20 | 21 | const header = { 22 | "alg": "RS256", 23 | "kid": bru.getEnvVar("client_kid") 24 | }; 25 | 26 | const payload = { 27 | "aud": "https://test.maskinporten.no/", 28 | "scope": "altinn:authentication/systemregister.write altinn:serviceowner altinn:authentication/systemuser.request.write altinn:authentication/systemuser.request.read", 29 | "iss": bru.getEnvVar("client_id"), 30 | "iat": Math.floor(Date.now() / 1000), 31 | "exp": Math.floor(Date.now() / 1000) + 120 32 | }; 33 | 34 | function base64url(input) { 35 | return Buffer.from(input).toString('base64') 36 | .replace(/\+/g, '-') 37 | .replace(/\//g, '_') 38 | .replace(/=/g, ''); 39 | } 40 | 41 | const encodedHeader = base64url(JSON.stringify(header)); 42 | const encodedPayload = base64url(JSON.stringify(payload)); 43 | const signatureInput = `${encodedHeader}.${encodedPayload}`; 44 | 45 | const privateKey = bru.getEnvVar("client_pem"); 46 | const signature = crypto.sign("RSA-SHA256", Buffer.from(signatureInput), privateKey); 47 | const encodedSignature = base64url(signature); 48 | 49 | const jwt = `${signatureInput}.${encodedSignature}`; 50 | bru.setVar("jwt", jwt); 51 | } 52 | 53 | script:post-response { 54 | const responseData = res.body; 55 | bru.setVar("systemprovider_maskinporten_token", responseData.access_token); 56 | } 57 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | volumes: 2 | broker-storage: 3 | postgres-data: 4 | 5 | services: 6 | storage: 7 | image: mcr.microsoft.com/azure-storage/azurite:latest 8 | ports: 9 | - "10000:10000" 10 | - "10001:10001" 11 | healthcheck: 12 | test: nc 127.0.0.1 10000 -z 13 | interval: 1s 14 | retries: 30 15 | storage_init: 16 | image: mcr.microsoft.com/azure-cli:latest 17 | command: 18 | - /bin/sh 19 | - -c 20 | - | 21 | az storage container create --name brokerfiles 22 | depends_on: 23 | storage: 24 | condition: service_healthy 25 | environment: 26 | AZURE_STORAGE_CONNECTION_STRING: DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://storage:10000/devstoreaccount1; 27 | database: 28 | image: 'postgres:latest' 29 | ports: 30 | - 5432:5432 31 | environment: 32 | POSTGRES_USER: postgres 33 | POSTGRES_PASSWORD: postgres 34 | POSTGRES_DB: broker 35 | volumes: 36 | - postgres-data:/var/lib/postgresql 37 | - ./tests/Altinn.Broker.Tests/Data/postgresql.conf:/etc/postgresql/postgresql.conf 38 | command: postgres -c config_file=/etc/postgresql/postgresql.conf 39 | database_migration: 40 | image: flyway/flyway:latest 41 | command: -url='jdbc:postgresql://database:5432/broker' -user=postgres -password=postgres -connectRetries=60 migrate -validateMigrationNaming='true' 42 | volumes: 43 | - ./src/Altinn.Broker.Persistence/Migrations:/flyway/sql 44 | - ./tests/Altinn.Broker.Tests/Data/:/flyway/sql/R__Prepare_Test_Data.sql 45 | depends_on: 46 | - database 47 | entrypoint: 48 | [ 49 | "flyway", 50 | ] 51 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.UseCaseTests/helpers/maskinportenJwtBuilder.js: -------------------------------------------------------------------------------- 1 | import encoding from 'k6/encoding'; 2 | import { pemToBinary, stringToBytes } from './cryptoUtils.js'; 3 | 4 | const sender = __ENV.sender; 5 | const recipient = __ENV.recipient; 6 | 7 | export async function buildMaskinportenJwt({ clientId, kid, pem, scope, tokenUrl, isSender }) { 8 | const now = Math.floor(Date.now() / 1000); 9 | const header = { alg: 'RS256', typ: 'JWT', kid }; 10 | const payload = { 11 | aud: tokenUrl, 12 | scope: scope, 13 | iss: clientId, 14 | sub: clientId, 15 | authorization_details: [ 16 | { 17 | type: "urn:altinn:systemuser", 18 | systemuser_org: 19 | { 20 | authority : "iso6523-actorid-upis", 21 | ID: isSender ? sender : recipient 22 | } 23 | } 24 | ], 25 | iat: now, 26 | nbf: now - 5, 27 | exp: now + 120, 28 | jti: `${now}-${Math.random().toString(36).slice(2)}` 29 | }; 30 | 31 | const encodedHeader = encoding.b64encode(JSON.stringify(header), 'url'); 32 | const encodedPayload = encoding.b64encode(JSON.stringify(payload), 'url'); 33 | const signingInput = `${encodedHeader}.${encodedPayload}`; 34 | 35 | const keyData = pemToBinary(pem); 36 | const key = await crypto.subtle.importKey( 37 | 'pkcs8', 38 | keyData, 39 | { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, 40 | false, 41 | ['sign'] 42 | ); 43 | 44 | const data = stringToBytes(signingInput); 45 | const signature = await crypto.subtle.sign({ name: 'RSASSA-PKCS1-v1_5' }, key, data); 46 | const encodedSignature = encoding.b64encode(new Uint8Array(signature), 'url'); 47 | return `${signingInput}.${encodedSignature}`; 48 | } -------------------------------------------------------------------------------- /src/Altinn.Broker.Common/ClaimsPrincipalExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using System.Text.Json; 3 | 4 | using Altinn.Broker.Common.Helpers.Models; 5 | 6 | namespace Altinn.Broker.Common; 7 | public static class ClaimsPrincipalExtensions 8 | { 9 | public static string? GetCallerOrganizationId(this ClaimsPrincipal user) 10 | { 11 | var claims = user.Claims; 12 | 13 | // System user token (from Maskinporten with authorization_details) 14 | var systemUserClaim = user.Claims.FirstOrDefault(c => c.Type == "authorization_details"); 15 | if (systemUserClaim is not null) 16 | { 17 | try 18 | { 19 | var systemUserAuthorizationDetails = JsonSerializer.Deserialize(systemUserClaim.Value); 20 | return systemUserAuthorizationDetails?.SystemUserOrg.ID.WithoutPrefix(); 21 | } 22 | catch (JsonException) 23 | { 24 | // Invalid JSON in authorization_details claim 25 | return null; 26 | } 27 | } 28 | 29 | // Enterprise token (from Altinn) 30 | var orgClaim = user.Claims.FirstOrDefault(c => c.Type == "urn:altinn:orgNumber"); 31 | if (orgClaim is not null) 32 | { 33 | return orgClaim.Value.WithoutPrefix(); // Normalize to same format as elsewhere 34 | } 35 | 36 | // Legacy Maskinporten token with consumer claim 37 | var consumerClaim = user.Claims.FirstOrDefault(c => c.Type == "consumer"); 38 | if (consumerClaim is not null) 39 | { 40 | var consumerObject = JsonSerializer.Deserialize(consumerClaim.Value); 41 | return consumerObject?.ID?.WithoutPrefix(); 42 | } 43 | 44 | return null; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Integrations/Hangfire/HangfireAppRequestFilter.cs: -------------------------------------------------------------------------------- 1 | using Hangfire.Server; 2 | 3 | using System.Diagnostics; 4 | 5 | namespace Altinn.Broker.Integrations.Hangfire; 6 | 7 | public class HangfireAppRequestFilter() : IServerFilter 8 | { 9 | private static readonly AsyncLocal _hangfireActivity = new(); 10 | private static readonly ActivitySource _activitySource = new("Altinn.Broker.Integrations.Hangfire"); 11 | 12 | public void OnPerformed(PerformedContext context) 13 | { 14 | _hangfireActivity.Value?.Stop(); 15 | } 16 | 17 | public void OnPerforming(PerformingContext context) 18 | { 19 | var operationName = $"HANGFIRE {context.BackgroundJob.Job.Method.DeclaringType?.Name}.{context.BackgroundJob.Job.Method.Name}"; 20 | 21 | var activity = _activitySource.StartActivity(operationName, ActivityKind.Server); 22 | if (activity != null) 23 | { 24 | activity.SetTag("hangfire.job.id", context.BackgroundJob.Id); 25 | activity.SetTag("hangfire.job.type", context.BackgroundJob.Job.Method.DeclaringType?.Name); 26 | activity.SetTag("hangfire.job.method", context.BackgroundJob.Job.Method.Name); 27 | activity.SetTag("hangfire.job.queue", context.BackgroundJob.Job.Queue); 28 | activity.SetTag("hangfire.job.created_at", context.BackgroundJob.CreatedAt.ToString("O")); 29 | 30 | // Add attributes that make it look like a request to Application Insights 31 | activity.SetTag("http.method", "HANGFIRE"); 32 | activity.SetTag("http.status_code", "102"); 33 | activity.SetTag("http.target", context.BackgroundJob.Id); 34 | activity.SetTag("http.host", "hangfire"); 35 | activity.SetTag("http.flavor", "1.1"); 36 | 37 | _hangfireActivity.Value = activity; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Altinn.Broker.Tests/Factories/FileTransferInitializeExtTestFactory.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Models; 2 | using Altinn.Broker.Tests.Helpers; 3 | 4 | namespace Altinn.Broker.Tests.Factories; 5 | internal static class FileTransferInitializeExtTestFactory 6 | { 7 | internal static FileTransferInitalizeExt BasicFileTransfer() => new FileTransferInitalizeExt() 8 | { 9 | ResourceId = TestConstants.RESOURCE_FOR_TEST, 10 | Checksum = null, 11 | FileName = "input.txt", 12 | PropertyList = [], 13 | Recipients = new List { "0192:986252932" }, 14 | Sender = "0192:991825827", 15 | SendersFileTransferReference = "test-data" 16 | }; 17 | 18 | internal static FileTransferInitalizeExt BasicFileTransfer2() => new FileTransferInitalizeExt() 19 | { 20 | ResourceId = TestConstants.RESOURCE_FOR_TEST, 21 | Checksum = null, 22 | FileName = "input2.txt", 23 | PropertyList = [], 24 | Recipients = new List { "0192:991825827" }, 25 | Sender = "0192:991825827", 26 | SendersFileTransferReference = "test-data-2" 27 | }; 28 | internal static FileTransferInitalizeExt BasicFileTransfer_MultipleRecipients() => new FileTransferInitalizeExt() 29 | { 30 | ResourceId = TestConstants.RESOURCE_FOR_TEST, 31 | Checksum = null, 32 | FileName = "input.txt", 33 | PropertyList = [], 34 | Recipients = new List { "0192:986252932", "0192:910351192" }, 35 | Sender = "0192:991825827", 36 | SendersFileTransferReference = "test-data" 37 | }; 38 | internal static FileTransferInitalizeExt BasicFileTransfer_ManifestShim() { 39 | var basicFileTransfer = BasicFileTransfer(); 40 | basicFileTransfer.ResourceId = TestConstants.RESOURCE_WITH_MANIFEST_SHIM; 41 | return basicFileTransfer; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/ci-cd.yaml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths-ignore: 7 | - "Test/**" # ignore changes to tests 8 | 9 | jobs: 10 | test: 11 | name: QA 12 | uses: ./.github/workflows/test-application.yml 13 | 14 | deploy-test: 15 | name: Internal test 16 | uses: ./.github/workflows/deploy-to-environment.yml 17 | if: always() && !failure() && !cancelled() 18 | needs: [test] 19 | permissions: 20 | id-token: write 21 | contents: read 22 | packages: write 23 | secrets: inherit 24 | with: 25 | environment: test 26 | 27 | deploy-staging: 28 | name: Staging 29 | needs: [ 30 | deploy-test, 31 | ] 32 | uses: ./.github/workflows/deploy-to-environment.yml 33 | if: (!failure() && !cancelled()) 34 | permissions: 35 | id-token: write 36 | contents: read 37 | packages: write 38 | secrets: inherit 39 | with: 40 | environment: staging 41 | 42 | deploy-production: 43 | name: Production 44 | needs: [ 45 | deploy-staging, 46 | ] 47 | uses: ./.github/workflows/deploy-to-environment.yml 48 | if: (!failure() && !cancelled()) 49 | permissions: 50 | id-token: write 51 | contents: read 52 | packages: write 53 | secrets: inherit 54 | with: 55 | environment: production 56 | 57 | release-to-git: 58 | name: Release to git 59 | runs-on: ubuntu-latest 60 | needs: [deploy-production] 61 | if: ${{ !failure() && !cancelled()}} 62 | permissions: 63 | id-token: write 64 | contents: write 65 | steps: 66 | - name: Checkout 67 | uses: actions/checkout@v4 68 | 69 | - name: release 70 | if: (!failure() && !cancelled()) 71 | uses: ./.github/actions/release-to-git 72 | with: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /tests/Altinn.Broker.UseCaseTests/helpers/altinnTokenService.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http'; 2 | import { retrieveMaskinportenToken } from './maskinportenTokenService.js'; 3 | 4 | const authorizationBaseUrl = (__ENV.base_url.toLowerCase().includes('platform.altinn.no')) 5 | ? 'https://platform.altinn.no' 6 | : 'https://platform.tt02.altinn.no'; 7 | 8 | async function retrieveAltinnToken({ baseUrl, clientId, kid, pem, scope, isSender }) { 9 | const mpToken = await retrieveMaskinportenToken({ clientId, kid, pem, scope, isSender }); 10 | const headers = { 'Authorization': `Bearer ${mpToken}`, 'Accept': 'application/json' }; 11 | const url = `${(baseUrl || '').replace(/\/$/, '')}/authentication/api/v1/exchange/maskinporten`; 12 | const res = http.get(url, { headers }); 13 | if (res.status !== 200) { 14 | throw new Error(`Altinn exchange failed: status=${res.status} body=${res.body}`); 15 | } 16 | const parsed = res.json(); 17 | if (typeof parsed === 'string') return parsed.replace(/^"|"$/g, ''); 18 | if (parsed && typeof parsed === 'object' && parsed.access_token) return parsed.access_token; 19 | throw new Error(`Altinn exchange failed: status=${res.status} body=${res.body}`); 20 | } 21 | 22 | export async function getSenderAltinnToken() { 23 | return await retrieveAltinnToken({ 24 | baseUrl: authorizationBaseUrl, 25 | clientId: __ENV.mp_client_id, 26 | kid: __ENV.mp_kid, 27 | pem: __ENV.mp_client_pem, 28 | scope: 'altinn:broker.write', 29 | isSender: true 30 | }); 31 | } 32 | 33 | export async function getRecipientAltinnToken() { 34 | return await retrieveAltinnToken({ 35 | baseUrl: authorizationBaseUrl, 36 | clientId: __ENV.mp_client_id, 37 | kid: __ENV.mp_kid, 38 | pem: __ENV.mp_client_pem, 39 | scope: 'altinn:broker.read', 40 | isSender: false 41 | }); 42 | } -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Helpers/MaskinportenHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | using Altinn.Broker.Models.Maskinporten; 4 | 5 | namespace Altinn.Broker.Helpers; 6 | 7 | public static class MaskinportenHelper 8 | { 9 | public static string? GetCallerFromTestToken(HttpContext httpContext) => httpContext.User.Claims.FirstOrDefault(claim => claim.Type == System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; 10 | 11 | public static string? GetConsumerFromToken(HttpContext httpContext) 12 | { 13 | var consumerClaim = httpContext.User.Claims.FirstOrDefault(claim => claim.Type == "consumer"); 14 | if (consumerClaim is null) 15 | { 16 | return null; 17 | } 18 | var consumer = JsonSerializer.Deserialize(consumerClaim.Value); 19 | return consumer?.ID; 20 | } 21 | 22 | public static string? GetSupplierFromToken(HttpContext httpContext) 23 | { 24 | var supplierClaim = httpContext.User.Claims.FirstOrDefault(claim => claim.Type == "supplier"); 25 | if (supplierClaim is null) 26 | { 27 | return null; 28 | } 29 | var supplier = JsonSerializer.Deserialize(supplierClaim.Value); 30 | return supplier?.ID; 31 | } 32 | 33 | public static string? GetScopeFromToken(HttpContext httpContext) 34 | { 35 | var scopeClaim = httpContext.User.Claims.FirstOrDefault(claim => claim.Type == "scope"); 36 | if (scopeClaim is null) 37 | { 38 | return null; 39 | } 40 | return scopeClaim.Value; 41 | } 42 | 43 | public static string? GetClientIdFromToken(HttpContext httpContext) => httpContext.User.Claims.FirstOrDefault(claim => claim.Type == "client_id")?.Value; 44 | 45 | public const string WriteScope = "altinn:broker.write"; 46 | public const string ReadScope = "altinn:broker.read"; 47 | } 48 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Application/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Broker.Application.ConfigureResource; 2 | using Altinn.Broker.Application.DownloadFile; 3 | using Altinn.Broker.Application.PurgeFileTransfer; 4 | using Altinn.Broker.Application.GetFileTransferDetails; 5 | using Altinn.Broker.Application.GetFileTransferOverview; 6 | using Altinn.Broker.Application.GetFileTransfers; 7 | using Altinn.Broker.Application.GetResource; 8 | using Altinn.Broker.Application.GenerateReport; 9 | using Altinn.Broker.Application.InitializeFileTransfer; 10 | using Altinn.Broker.Application.Middlewares; 11 | using Altinn.Broker.Application.UploadFile; 12 | using Altinn.Broker.Application.CleanupUseCaseTests; 13 | 14 | using Microsoft.Extensions.DependencyInjection; 15 | 16 | namespace Altinn.Broker.Application; 17 | public static class DependencyInjection 18 | { 19 | public static void AddApplicationHandlers(this IServiceCollection services) 20 | { 21 | services.AddScoped(); 22 | services.AddScoped(); 23 | services.AddScoped(); 24 | services.AddScoped(); 25 | services.AddScoped(); 26 | services.AddScoped(); 27 | services.AddScoped(); 28 | services.AddScoped(); 29 | services.AddScoped(); 30 | services.AddScoped(); 31 | services.AddScoped(); 32 | services.AddScoped(); 33 | services.AddScoped(); 34 | services.AddScoped(); 35 | services.AddScoped(); 36 | services.AddScoped(); 37 | services.AddScoped(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Altinn.Broker.Core/Helpers/TransactionWithRetriesPolicy.cs: -------------------------------------------------------------------------------- 1 | using System.Transactions; 2 | 3 | using Hangfire; 4 | using Hangfire.PostgreSql; 5 | 6 | using Microsoft.Extensions.Logging; 7 | 8 | using Npgsql; 9 | 10 | using Polly; 11 | using Polly.Retry; 12 | 13 | namespace Altinn.Broker.Core.Helpers; 14 | public static class TransactionWithRetriesPolicy 15 | { 16 | public static async Task Execute( 17 | Func> operation, 18 | ILogger logger, 19 | CancellationToken cancellationToken = default) 20 | { 21 | var result = await RetryPolicy(logger).ExecuteAndCaptureAsync(async (cancellationToken) => 22 | { 23 | using var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); 24 | var result = await operation(cancellationToken); 25 | transaction.Complete(); 26 | return result; 27 | }, cancellationToken); 28 | if (result.Outcome == OutcomeType.Failure) 29 | { 30 | logger.LogError("Exception during retries: {message}\n{stackTrace}", result.FinalException.Message, result.FinalException.StackTrace); 31 | throw result.FinalException; 32 | } 33 | return result.Result; 34 | } 35 | 36 | public static AsyncRetryPolicy RetryPolicy(ILogger logger) => Policy 37 | .Handle() 38 | .Or() 39 | .Or() 40 | .Or() 41 | .WaitAndRetryAsync( 42 | 8, 43 | retryAttempt => TimeSpan.FromMilliseconds(Math.Min(5 * Math.Pow(2, retryAttempt), 640)), 44 | (exception, timeSpan, retryCount, context) => 45 | { 46 | logger.LogWarning($"Attempt {retryCount} failed with exception {exception.Message}. Retrying in {timeSpan.Milliseconds} seconds."); 47 | } 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/Altinn.Broker.API/Helpers/ValidateUseManifestFileShim.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Altinn.Broker.Helpers 4 | { 5 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] 6 | public class ValidateUseManifestFileShim : ValidationAttribute 7 | { 8 | protected override ValidationResult IsValid(object value, ValidationContext validationContext) 9 | { 10 | var useManifestFileShimProperty = validationContext.ObjectType.GetProperty("UseManifestFileShim"); 11 | var externalServiceCodeLegacyProperty = validationContext.ObjectType.GetProperty("ExternalServiceCodeLegacy"); 12 | var externalServiceEditionCodeLegacyProperty = validationContext.ObjectType.GetProperty("ExternalServiceEditionCodeLegacy"); 13 | var useManifestFileShimValue = (bool?)useManifestFileShimProperty?.GetValue(validationContext.ObjectInstance, null); 14 | var externalServiceCodeLegacyValue = externalServiceCodeLegacyProperty?.GetValue(validationContext.ObjectInstance, null); 15 | var externalServiceEditionCodeLegacyValue = externalServiceEditionCodeLegacyProperty?.GetValue(validationContext.ObjectInstance, null); 16 | 17 | if (useManifestFileShimValue == true) 18 | { 19 | if (externalServiceCodeLegacyValue == null || (externalServiceCodeLegacyValue is string strValue && string.IsNullOrEmpty(strValue))) 20 | { 21 | return new ValidationResult("ExternalServiceCodeLegacy must be set and not be an empty string."); 22 | } 23 | 24 | if (externalServiceEditionCodeLegacyValue == null || (externalServiceEditionCodeLegacyValue is int intValue && intValue == 0)) 25 | { 26 | return new ValidationResult("ExternalServiceEditionCodeLegacy must be set and not be zero."); 27 | } 28 | } 29 | return ValidationResult.Success; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.bruno/Authentication/Create sender system user Maskinporten token.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Create sender system user Maskinporten token 3 | type: http 4 | seq: 3 5 | } 6 | 7 | post { 8 | url: https://test.maskinporten.no/token 9 | body: formUrlEncoded 10 | auth: none 11 | } 12 | 13 | body:form-urlencoded { 14 | grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer 15 | assertion: {{jwt}} 16 | } 17 | 18 | script:pre-request { 19 | const crypto = require('crypto'); 20 | 21 | const header = { 22 | "alg": "RS256", 23 | "kid": bru.getEnvVar("client_kid") 24 | }; 25 | 26 | const payload = { 27 | "aud": "https://test.maskinporten.no/", 28 | "scope": "altinn:broker.write", 29 | "iss": bru.getEnvVar("client_id"), 30 | "iat": Math.floor(Date.now() / 1000), 31 | "exp": Math.floor(Date.now() / 1000) + 120, 32 | "authorization_details": [ 33 | { 34 | "type": "urn:altinn:systemuser", 35 | "systemuser_org": { 36 | "authority": "iso6523-actorid-upis", 37 | "ID": bru.getEnvVar("sender_orgnumber") 38 | } 39 | } 40 | ] 41 | }; 42 | 43 | function base64url(input) { 44 | return Buffer.from(input).toString('base64') 45 | .replace(/\+/g, '-') 46 | .replace(/\//g, '_') 47 | .replace(/=/g, ''); 48 | } 49 | 50 | const encodedHeader = base64url(JSON.stringify(header)); 51 | const encodedPayload = base64url(JSON.stringify(payload)); 52 | const signatureInput = `${encodedHeader}.${encodedPayload}`; 53 | 54 | const privateKey = bru.getEnvVar("client_pem"); 55 | const signature = crypto.sign("RSA-SHA256", Buffer.from(signatureInput), privateKey); 56 | const encodedSignature = base64url(signature); 57 | 58 | const jwt = `${signatureInput}.${encodedSignature}`; 59 | bru.setVar("jwt", jwt); 60 | } 61 | 62 | script:post-response { 63 | const responseData = res.body; 64 | bru.setVar("sender_maskinporten_token", responseData.access_token); 65 | } 66 | -------------------------------------------------------------------------------- /.bruno/Authentication/Create recipient system user maskinporten token.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Create recipient system user Maskinporten token 3 | type: http 4 | seq: 5 5 | } 6 | 7 | post { 8 | url: https://test.maskinporten.no/token 9 | body: formUrlEncoded 10 | auth: none 11 | } 12 | 13 | body:form-urlencoded { 14 | grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer 15 | assertion: {{jwt}} 16 | } 17 | 18 | script:pre-request { 19 | const crypto = require('crypto'); 20 | 21 | const header = { 22 | "alg": "RS256", 23 | "kid": bru.getEnvVar("client_kid") 24 | }; 25 | 26 | const payload = { 27 | "aud": "https://test.maskinporten.no/", 28 | "scope": "altinn:broker.read", 29 | "iss": bru.getEnvVar("client_id"), 30 | "iat": Math.floor(Date.now() / 1000), 31 | "exp": Math.floor(Date.now() / 1000) + 120, 32 | "authorization_details": [ 33 | { 34 | "type": "urn:altinn:systemuser", 35 | "systemuser_org": { 36 | "authority": "iso6523-actorid-upis", 37 | "ID": bru.getEnvVar("recipient_orgnumber") 38 | } 39 | } 40 | ] 41 | }; 42 | 43 | function base64url(input) { 44 | return Buffer.from(input).toString('base64') 45 | .replace(/\+/g, '-') 46 | .replace(/\//g, '_') 47 | .replace(/=/g, ''); 48 | } 49 | 50 | const encodedHeader = base64url(JSON.stringify(header)); 51 | const encodedPayload = base64url(JSON.stringify(payload)); 52 | const signatureInput = `${encodedHeader}.${encodedPayload}`; 53 | 54 | const privateKey = bru.getEnvVar("client_pem"); 55 | const signature = crypto.sign("RSA-SHA256", Buffer.from(signatureInput), privateKey); 56 | const encodedSignature = base64url(signature); 57 | 58 | const jwt = `${signatureInput}.${encodedSignature}`; 59 | bru.setVar("jwt", jwt); 60 | } 61 | 62 | script:post-response { 63 | const responseData = res.body; 64 | bru.setVar("recipient_maskinporten_token", responseData.access_token); 65 | } 66 | --------------------------------------------------------------------------------