Skip to content

Commit 4d21040

Browse files
CeredronRoar Mjelde
andauthored
Improper handling of Unauthorized in Initialize-and-Upload endpoint (#809)
* Fix initialize-and-upload endpoint * We should use form binding here in the same way as we do in Correspondence --------- Co-authored-by: Roar Mjelde <[email protected]>
1 parent c0000b8 commit 4d21040

File tree

4 files changed

+214
-42
lines changed

4 files changed

+214
-42
lines changed

.bruno/FileTransfer/Initialize and Upload.bru

Lines changed: 0 additions & 38 deletions
This file was deleted.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
}

src/Altinn.Broker.API/Controllers/FileTransferController.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,18 +161,21 @@ CancellationToken cancellationToken
161161
var initializeResult = await initializeFileTransferHandler.Process(initializeRequest, HttpContext.User, cancellationToken);
162162
if (initializeResult.IsT1)
163163
{
164-
Problem(initializeResult.AsT1);
164+
return Problem(initializeResult.AsT1);
165165
}
166166
var fileTransferId = initializeResult.AsT0;
167167

168-
Request.EnableBuffering();
169168
var uploadResult = await UploadFileHandler.Process(new UploadFileRequest()
170169
{
171170
FileTransferId = fileTransferId,
172-
UploadStream = Request.Body
171+
UploadStream = form.FileTransfer.OpenReadStream(),
172+
ContentLength = form.FileTransfer.Length
173173
}, HttpContext.User, cancellationToken);
174174
return uploadResult.Match(
175-
FileId => Ok(FileId.ToString()),
175+
uploadedFileTransferId => Ok(new FileTransferUploadResponseExt()
176+
{
177+
FileTransferId = uploadedFileTransferId
178+
}),
176179
Problem
177180
);
178181
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
using System.Net;
2+
using System.Net.Http.Headers;
3+
using System.Net.Http.Json;
4+
using System.Text;
5+
using System.Text.Json;
6+
7+
using Altinn.Broker.API.Models;
8+
using Altinn.Broker.Enums;
9+
using Altinn.Broker.Models;
10+
using Altinn.Broker.Tests.Helpers;
11+
12+
using Microsoft.AspNetCore.Mvc;
13+
14+
using Xunit;
15+
16+
namespace Altinn.Broker.Tests;
17+
18+
public class InitializeAndUploadFormDataTests : IClassFixture<CustomWebApplicationFactory>
19+
{
20+
private readonly CustomWebApplicationFactory _factory;
21+
private readonly HttpClient _senderClient;
22+
private readonly HttpClient _recipientClient;
23+
private readonly JsonSerializerOptions _jsonOptions;
24+
25+
public InitializeAndUploadFormDataTests(CustomWebApplicationFactory factory)
26+
{
27+
_factory = factory;
28+
_senderClient = _factory.CreateClientWithAuthorization(TestConstants.DUMMY_SENDER_TOKEN);
29+
_recipientClient = _factory.CreateClientWithAuthorization(TestConstants.DUMMY_RECIPIENT_TOKEN);
30+
_jsonOptions = new JsonSerializerOptions
31+
{
32+
PropertyNameCaseInsensitive = true
33+
};
34+
_jsonOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
35+
}
36+
37+
[Fact]
38+
public async Task InitializeAndUpload_WhenAllIsOK_Success()
39+
{
40+
// Arrange
41+
var fileContent = "This is the contents of the uploaded file";
42+
var metadata = new FileTransferInitalizeExt
43+
{
44+
FileName = "test.txt",
45+
ResourceId = TestConstants.RESOURCE_FOR_TEST,
46+
Sender = "0192:991825827",
47+
SendersFileTransferReference = "CaseFiles-123",
48+
Recipients = new List<string> { "0192:986252932" },
49+
PropertyList = new Dictionary<string, string> { { "key1", "value1" } }
50+
};
51+
52+
// Act - Initialize and upload in one request
53+
var multipart = CreateMultipartFormData(metadata, fileContent);
54+
var response = await _senderClient.PostAsync("broker/api/v1/filetransfer/upload", multipart);
55+
56+
// Assert - Upload succeeded
57+
Assert.True(response.IsSuccessStatusCode, await response.Content.ReadAsStringAsync());
58+
var uploadResponse = await response.Content.ReadFromJsonAsync<FileTransferUploadResponseExt>(_jsonOptions);
59+
Assert.NotNull(uploadResponse);
60+
var fileTransferId = uploadResponse.FileTransferId.ToString();
61+
62+
// Verify file transfer status is Published
63+
var fileTransferOverview = await _senderClient.GetFromJsonAsync<FileTransferOverviewExt>($"broker/api/v1/filetransfer/{fileTransferId}", _jsonOptions);
64+
Assert.NotNull(fileTransferOverview);
65+
Assert.Equal(FileTransferStatusExt.Published, fileTransferOverview.FileTransferStatus);
66+
67+
// Verify file can be downloaded by recipient
68+
var downloadedFile = await _recipientClient.GetAsync($"broker/api/v1/filetransfer/{fileTransferId}/download");
69+
Assert.True(downloadedFile.IsSuccessStatusCode);
70+
var downloadedFileBytes = await downloadedFile.Content.ReadAsByteArrayAsync();
71+
var expectedFileBytes = Encoding.UTF8.GetBytes(fileContent);
72+
Assert.Equal(expectedFileBytes, downloadedFileBytes);
73+
}
74+
75+
[Fact]
76+
public async Task InitializeAndUpload_UnauthorizedSender_Returns401()
77+
{
78+
var metadata = new FileTransferInitalizeExt
79+
{
80+
FileName = "test.txt",
81+
ResourceId = TestConstants.RESOURCE_WITH_NO_ACCESS,
82+
Sender = "0192:991825827",
83+
Recipients = new List<string> { "0192:986252932" },
84+
SendersFileTransferReference = "123",
85+
PropertyList = new Dictionary<string, string> { { "k1", "v1" } }
86+
};
87+
88+
var multipart = CreateMultipartFormData(metadata, "hello world");
89+
90+
var resp = await _senderClient.PostAsync("broker/api/v1/filetransfer/upload", multipart);
91+
92+
Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
93+
var problem = await resp.Content.ReadFromJsonAsync<ProblemDetails>(_jsonOptions);
94+
Assert.NotNull(problem);
95+
Assert.Contains("You must use a bearer token that represents a system user with access to the resource in the Resource Rights Registry", problem.Detail);
96+
}
97+
98+
[Fact]
99+
public async Task InitializeAndUpload_MissingFile_Returns400()
100+
{
101+
var metadata = new FileTransferInitalizeExt
102+
{
103+
FileName = "test.txt",
104+
ResourceId = TestConstants.RESOURCE_FOR_TEST,
105+
Sender = "0192:991825827",
106+
SendersFileTransferReference = "123",
107+
Recipients = new List<string> { "0192:986252932" },
108+
PropertyList = new Dictionary<string, string>()
109+
};
110+
111+
var multipart = new MultipartFormDataContent();
112+
113+
// Add metadata fields but intentionally omit the FileTransfer part
114+
multipart.Add(new StringContent(metadata.FileName), "Metadata.FileName");
115+
multipart.Add(new StringContent(metadata.ResourceId), "Metadata.ResourceId");
116+
multipart.Add(new StringContent(metadata.Sender), "Metadata.Sender");
117+
foreach (var recipient in metadata.Recipients)
118+
{
119+
multipart.Add(new StringContent(recipient), "Metadata.Recipients");
120+
}
121+
122+
var resp = await _senderClient.PostAsync("broker/api/v1/filetransfer/upload", multipart);
123+
124+
Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
125+
var problem = await resp.Content.ReadFromJsonAsync<ProblemDetails>(_jsonOptions);
126+
Assert.NotNull(problem);
127+
}
128+
129+
private MultipartFormDataContent CreateMultipartFormData(FileTransferInitalizeExt metadata, string fileContent)
130+
{
131+
var multipart = new MultipartFormDataContent();
132+
133+
// Add metadata as individual form fields with Metadata. prefix
134+
multipart.Add(new StringContent(metadata.FileName), "Metadata.FileName");
135+
multipart.Add(new StringContent(metadata.ResourceId), "Metadata.ResourceId");
136+
if (!string.IsNullOrEmpty(metadata.SendersFileTransferReference))
137+
{
138+
multipart.Add(new StringContent(metadata.SendersFileTransferReference), "Metadata.SendersFileTransferReference");
139+
}
140+
multipart.Add(new StringContent(metadata.Sender), "Metadata.Sender");
141+
142+
// Add recipients - ASP.NET Core model binding should handle multiple values with the same key
143+
foreach (var recipient in metadata.Recipients)
144+
{
145+
multipart.Add(new StringContent(recipient), "Metadata.Recipients");
146+
}
147+
148+
if (metadata.DisableVirusScan.HasValue)
149+
{
150+
multipart.Add(new StringContent(metadata.DisableVirusScan.Value.ToString().ToLower()), "Metadata.DisableVirusScan");
151+
}
152+
153+
// Add property list if present
154+
if (metadata.PropertyList != null && metadata.PropertyList.Count > 0)
155+
{
156+
foreach (var property in metadata.PropertyList)
157+
{
158+
multipart.Add(new StringContent(property.Value), $"Metadata.PropertyList[{property.Key}]");
159+
}
160+
}
161+
162+
if (!string.IsNullOrEmpty(metadata.Checksum))
163+
{
164+
multipart.Add(new StringContent(metadata.Checksum), "Metadata.Checksum");
165+
}
166+
167+
// Add file
168+
var fileBytes = Encoding.UTF8.GetBytes(fileContent);
169+
var fileContentBytes = new ByteArrayContent(fileBytes);
170+
fileContentBytes.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
171+
multipart.Add(fileContentBytes, nameof(FileTransferInitializeAndUploadExt.FileTransfer), "FileTransfer");
172+
173+
return multipart;
174+
}
175+
}
176+
177+

0 commit comments

Comments
 (0)