Skip to content

Commit fdb4d0a

Browse files
acn-dgopaDhanalakshmi Gopalswamymgunnerud
authored
extend delegation problem with reasoncodes for systemuser delegation (#2387)
* extend delegation problem with reasoncodes and actual failing delegation package and resource name * fix PR comments * add more tests to cover multiple errors and reasons * resource id instead of resourcename in delegation error * map specific systemuser delegation errors from backend * handle bad json in delegationReasons * show resource errors for system user approval as list * type fix * fix key * translate single error line * proper fix * Error title for resources/accespackages --------- Co-authored-by: Dhanalakshmi Gopalswamy <acn-dgopa@ai-dev.no> Co-authored-by: Martin Gunnerud <mgunnerud@gmail.com>
1 parent a304d1b commit fdb4d0a

15 files changed

Lines changed: 389 additions & 26 deletions

File tree

backend/src/Altinn.AccessManagement.UI/Altinn.AccessManagement.UI.Core/Constants/Problem.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,24 @@ private static readonly ProblemDescriptorFactory _factory
167167
public static ProblemDescriptor DelegationRightMissingPackageAccess { get; }
168168
= _factory.Create(68, HttpStatusCode.Forbidden, "DelegationCheck failed with error: missing required access package for delegation.");
169169

170+
/// <summary>
171+
/// Gets a <see cref="ProblemDescriptor"/>.
172+
/// </summary>
173+
public static ProblemDescriptor DelegationRightAccessListValidationFail { get; }
174+
= _factory.Create(69, HttpStatusCode.Forbidden, "DelegationCheck failed with error: The receiver does not have the right based on Access List delegation.");
175+
176+
/// <summary>
177+
/// Gets a <see cref="ProblemDescriptor"/>.
178+
/// </summary>
179+
public static ProblemDescriptor DelegationRightResourceNotDelegable { get; }
180+
= _factory.Create(70, HttpStatusCode.Forbidden, "DelegationCheck failed with error: The resource cannot be delegated to another user or entity.");
181+
182+
/// <summary>
183+
/// Gets a <see cref="ProblemDescriptor"/>.
184+
/// </summary>
185+
public static ProblemDescriptor DelegationRightResourceIsMaskinPortenSchema { get; }
186+
= _factory.Create(71, HttpStatusCode.Forbidden, "DelegationCheck failed with error: The resource is not delegable because it is a Maskinporten schema resource.");
187+
170188
/// <summary>
171189
/// Gets a <see cref="ProblemDescriptor"/>.
172190
/// </summary>

backend/src/Altinn.AccessManagement.UI/Altinn.AccessManagement.UI.Core/Helpers/ProblemMapper.cs

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Generic;
12
using System.Net;
23
using System.Text.Json;
34
using Altinn.AccessManagement.UI.Core.Constants;
@@ -10,17 +11,24 @@ namespace Altinn.AccessManagement.UI.Core.Helpers
1011
/// </summary>
1112
public static class ProblemMapper
1213
{
14+
/// <summary>
15+
/// Extension members forwarded verbatim from the upstream (authentication) problem so the
16+
/// specifics (e.g. which access package/right could not be delegated and why) survive to the
17+
/// frontend instead of being flattened to just the error code.
18+
/// </summary>
19+
private static readonly string[] ForwardableExtensionKeys = ["delegationReasons"];
20+
1321
/// <summary>
1422
/// Map error codes from AUTH to AMUI error codes
1523
/// </summary>
16-
public static ProblemDescriptor MapToAuthUiError(string responseContent, HttpStatusCode statusCode)
24+
public static ProblemInstance MapToAuthUiError(string responseContent, HttpStatusCode statusCode)
1725
{
1826
try
1927
{
2028
AltinnProblemDetails problemDetails = JsonSerializer.Deserialize<AltinnProblemDetails>(responseContent);
2129
string authErrorCode = problemDetails?.ErrorCode.ToString();
2230

23-
return authErrorCode switch
31+
ProblemDescriptor descriptor = authErrorCode switch
2432
{
2533
"AUTH-00001" => Problem.Rights_NotFound_Or_NotDelegable,
2634
"AUTH-00002" => Problem.Rights_FailedToDelegate,
@@ -44,17 +52,59 @@ public static ProblemDescriptor MapToAuthUiError(string responseContent, HttpSta
4452
"AUTH-00062" => Problem.SystemUser_FailedToGetDelegatedRights,
4553
"AUTH-00066" => Problem.Request_UserIsNotAccessManager,
4654
"AUTH-00068" => Problem.DelegationRightMissingPackageAccess,
55+
"AUTH-00069" => Problem.DelegationRightAccessListValidationFail,
56+
"AUTH-00070" => Problem.DelegationRightResourceNotDelegable,
57+
"AUTH-00071" => Problem.DelegationRightResourceIsMaskinPortenSchema,
4758

4859
_ => Problem.Generic_EndOfMethod,
4960
};
61+
62+
// Forward the detail extensions (e.g. "delegationReasons") from the upstream problem so
63+
// the reason survives to the frontend instead of being reduced to just the error code.
64+
List<KeyValuePair<string, string>> forwarded = ExtractForwardableExtensions(problemDetails);
65+
66+
// Always return a ProblemInstance explicitly (never rely on the implicit
67+
// ProblemDescriptor -> ProblemInstance conversion) so the return type is consistent.
68+
return forwarded.Count > 0
69+
? descriptor.Create(ProblemExtensionData.Create([.. forwarded]))
70+
: descriptor.Create();
5071
}
5172
catch
5273
{
53-
// In case of deserialization failure or any other exception, return a generic problem descriptor
54-
return Problem.CreateGenericProblem(statusCode, "Error without problem code");
74+
// In case of deserialization failure or any other exception, return a generic problem instance.
75+
return Problem.CreateGenericProblem(statusCode, "Error without problem code").Create();
5576
}
5677
}
5778

79+
private static List<KeyValuePair<string, string>> ExtractForwardableExtensions(AltinnProblemDetails problemDetails)
80+
{
81+
List<KeyValuePair<string, string>> forwarded = [];
82+
83+
if (problemDetails?.Extensions is null)
84+
{
85+
return forwarded;
86+
}
87+
88+
foreach (string key in ForwardableExtensionKeys)
89+
{
90+
if (!problemDetails.Extensions.TryGetValue(key, out object value) || value is null)
91+
{
92+
continue;
93+
}
94+
95+
string text = value is JsonElement element
96+
? (element.ValueKind == JsonValueKind.String ? element.GetString() : element.ToString())
97+
: value.ToString();
98+
99+
if (!string.IsNullOrWhiteSpace(text))
100+
{
101+
forwarded.Add(new KeyValuePair<string, string>(key, text));
102+
}
103+
}
104+
105+
return forwarded;
106+
}
107+
58108
/// <summary>
59109
/// Map error codes from AM to AMUI error codes
60110
/// </summary>

backend/src/Altinn.AccessManagement.UI/Altinn.AccessManagement.UI.Tests/Helpers/ProblemMapperTest.cs

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public void MapToAuthUiError_ShouldReturnError1()
1616
string responseContent = "{ \"code\": \"" + errorCode + "\" }";
1717

1818
// Act
19-
ProblemDescriptor actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
19+
ProblemInstance actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
2020

2121
// Assert
2222
Assert.Equal(expectedErrorCode, actualError.ErrorCode.ToString());
@@ -31,7 +31,7 @@ public void MapToAuthUiError_ShouldReturnError2()
3131
string responseContent = "{ \"code\": \"" + errorCode + "\" }";
3232

3333
// Act
34-
ProblemDescriptor actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
34+
ProblemInstance actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
3535
// Assert
3636
Assert.Equal(expectedErrorCode, actualError.ErrorCode.ToString());
3737
}
@@ -45,7 +45,7 @@ public void MapToAuthUiError_ShouldReturnError3()
4545
string responseContent = "{ \"code\": \"" + errorCode + "\" }";
4646

4747
// Act
48-
ProblemDescriptor actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
48+
ProblemInstance actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
4949

5050
// Assert
5151
Assert.Equal(expectedErrorCode, actualError.ErrorCode.ToString());
@@ -60,7 +60,7 @@ public void MapToAuthUiError_ShouldReturnError4()
6060
string responseContent = "{ \"code\": \"" + errorCode + "\" }";
6161

6262
// Act
63-
ProblemDescriptor actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
63+
ProblemInstance actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
6464

6565
// Assert
6666
Assert.Equal(expectedErrorCode, actualError.ErrorCode.ToString());
@@ -75,7 +75,7 @@ public void MapToAuthUiError_ShouldReturnError11()
7575
string responseContent = "{ \"code\": \"" + errorCode + "\" }";
7676

7777
// Act
78-
ProblemDescriptor actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
78+
ProblemInstance actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
7979

8080
// Assert
8181
Assert.Equal(expectedErrorCode, actualError.ErrorCode.ToString());
@@ -90,7 +90,7 @@ public void MapToAuthUiError_ShouldReturnError14()
9090
string responseContent = "{ \"code\": \"" + errorCode + "\" }";
9191

9292
// Act
93-
ProblemDescriptor actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
93+
ProblemInstance actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
9494

9595
// Assert
9696
Assert.Equal(expectedErrorCode, actualError.ErrorCode.ToString());
@@ -105,7 +105,7 @@ public void MapToAuthUiError_ShouldReturnError16()
105105
string responseContent = "{ \"code\": \"" + errorCode + "\" }";
106106

107107
// Act
108-
ProblemDescriptor actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
108+
ProblemInstance actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
109109

110110
// Assert
111111
Assert.Equal(expectedErrorCode, actualError.ErrorCode.ToString());
@@ -120,7 +120,7 @@ public void MapToAuthUiError_ShouldReturnError18()
120120
string responseContent = "{ \"code\": \"" + errorCode + "\" }";
121121

122122
// Act
123-
ProblemDescriptor actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
123+
ProblemInstance actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
124124

125125
// Assert
126126
Assert.Equal(expectedErrorCode, actualError.ErrorCode.ToString());
@@ -135,7 +135,7 @@ public void MapToAuthUiError_ShouldReturnError19()
135135
string responseContent = "{ \"code\": \"" + errorCode + "\" }";
136136

137137
// Act
138-
ProblemDescriptor actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
138+
ProblemInstance actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
139139

140140
// Assert
141141
Assert.Equal(expectedErrorCode, actualError.ErrorCode.ToString());
@@ -150,7 +150,7 @@ public void MapToAuthUiError_ShouldReturnError20()
150150
string responseContent = "{ \"code\": \"" + errorCode + "\" }";
151151

152152
// Act
153-
ProblemDescriptor actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
153+
ProblemInstance actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
154154

155155
// Assert
156156
Assert.Equal(expectedErrorCode, actualError.ErrorCode.ToString());
@@ -165,7 +165,7 @@ public void MapToAuthUiError_ShouldGenericError()
165165
string responseContent = "{ \"code\": \"" + errorCode + "\" }";
166166

167167
// Act
168-
ProblemDescriptor actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
168+
ProblemInstance actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
169169

170170
// Assert
171171
Assert.Equal(expectedErrorCode, actualError.ErrorCode.ToString());
@@ -179,11 +179,61 @@ public void MapToAuthUiError_ShouldShowGenericProblemForErrorWithoutCode()
179179
var expectedStatusCode = HttpStatusCode.Forbidden;
180180

181181
// Act
182-
ProblemDescriptor actualError = ProblemMapper.MapToAuthUiError("", HttpStatusCode.Forbidden);
182+
ProblemInstance actualError = ProblemMapper.MapToAuthUiError("", HttpStatusCode.Forbidden);
183183

184184
// Assert
185185
Assert.Equal(expectedErrorCode, actualError.ErrorCode.ToString());
186186
Assert.Equal(expectedStatusCode, actualError.StatusCode);
187187
}
188+
189+
[Fact]
190+
public void MapToAuthUiError_ForwardsDelegationReasonsExtension()
191+
{
192+
// Arrange - the upstream (authentication) problem carries the structured delegationReasons
193+
// extension (a JSON string of resource id + codes). JsonSerializer.Serialize encodes it as a
194+
// JSON string value inside the response body.
195+
string reasons = """[{"type":"resource","id":"resource-a","codes":["MissingRoleAccess"]}]""";
196+
string responseContent = "{ \"code\": \"AUTH-00016\", \"delegationReasons\": " + System.Text.Json.JsonSerializer.Serialize(reasons) + " }";
197+
198+
// Act
199+
ProblemInstance actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
200+
201+
// Assert - the code is mapped AND the structured extension is preserved verbatim on the returned problem
202+
Assert.Equal("AMUI-00016", actualError.ErrorCode.ToString());
203+
Assert.True(actualError.Extensions.TryGetValue("delegationReasons", out string reason));
204+
Assert.Equal(reasons, reason);
205+
}
206+
207+
[Fact]
208+
public void MapToAuthUiError_WithoutExtensions_DoesNotAddDelegationReasons()
209+
{
210+
// Arrange - no extension members on the upstream problem
211+
string responseContent = "{ \"code\": \"AUTH-00016\" }";
212+
213+
// Act
214+
ProblemInstance actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
215+
216+
// Assert - mapped as before, and no delegationReasons is invented
217+
Assert.Equal("AMUI-00016", actualError.ErrorCode.ToString());
218+
Assert.False(actualError.Extensions.TryGetValue("delegationReasons", out _));
219+
}
220+
221+
[Fact]
222+
public void MapToAuthUiError_ForwardsDelegationReasons_MultipleResourcesAndCodes()
223+
{
224+
// Arrange - a realistic structured delegationReasons value spanning multiple resources, each
225+
// with one or more reason codes (as produced by authentication's DelegationHelper). The BFF
226+
// must forward the whole value verbatim - it must not truncate to the first resource/code.
227+
string reasons = """[{"type":"resource","id":"resource-a","codes":["MissingRoleAccess","MissingPackageAccess"]},{"type":"resource","id":"resource-b","codes":["ResourceIsMaskinPortenSchema"]}]""";
228+
string responseContent = "{ \"code\": \"AUTH-00016\", \"delegationReasons\": " + System.Text.Json.JsonSerializer.Serialize(reasons) + " }";
229+
230+
// Act
231+
ProblemInstance actualError = ProblemMapper.MapToAuthUiError(responseContent, HttpStatusCode.BadRequest);
232+
233+
// Assert - the full multi-resource / multi-code structured value is preserved unchanged
234+
Assert.Equal("AMUI-00016", actualError.ErrorCode.ToString());
235+
Assert.True(actualError.Extensions.TryGetValue("delegationReasons", out string reason));
236+
Assert.Equal(reasons, reason);
237+
}
188238
}
189239
}

src/features/amUI/systemUser/CreateSystemUserPage/RightsIncluded.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ export const RightsIncluded = ({ selectedSystem, onNavigateBack }: RightsInclude
9191
/>
9292
{createSystemUserError && (
9393
<DelegationCheckError
94+
accessPackages={rights?.accessPackages ?? []}
95+
resources={rights?.resources ?? []}
9496
defaultError='systemuser_includedrightspage.create_systemuser_error'
9597
error={createSystemUserError as { data: ProblemDetail }}
9698
/>

src/features/amUI/systemUser/SystemUserAgentDelegationPage/SystemUserAgentDelegationPageContent.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,12 +440,16 @@ export const SystemUserAgentDelegationPageContent = ({
440440
)}
441441
{assignSelfError && (
442442
<DelegationCheckError
443+
accessPackages={systemUser.accessPackages}
444+
resources={systemUser.resources}
443445
error={assignSelfError as { data: ProblemDetail }}
444446
defaultError={t('systemuser_agent_delegation.add_own_organization_error')}
445447
/>
446448
)}
447449
{removeSelfError && (
448450
<DelegationCheckError
451+
accessPackages={systemUser.accessPackages}
452+
resources={systemUser.resources}
449453
error={removeSelfError as { data: ProblemDetail }}
450454
defaultError={t('systemuser_agent_delegation.remove_own_organization_error')}
451455
/>

src/features/amUI/systemUser/SystemUserAgentRequestPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ export const SystemUserAgentRequestPage = () => {
171171
<div>
172172
{acceptCreationRequestError && (
173173
<DelegationCheckError
174+
accessPackages={request.accessPackages}
175+
resources={request.resources}
174176
defaultError='systemuser_includedrightspage.create_systemuser_error'
175177
error={acceptCreationRequestError as { data: ProblemDetail }}
176178
/>

src/features/amUI/systemUser/SystemUserChangeRequestPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ export const SystemUserChangeRequestPage = () => {
160160
<div>
161161
{acceptChangeRequestError && (
162162
<DelegationCheckError
163+
accessPackages={changeRequest.requiredAccessPackages}
164+
resources={changeRequest.requiredRights}
163165
defaultError='systemuser_change_request.accept_error'
164166
error={acceptChangeRequestError as { data: ProblemDetail }}
165167
/>

src/features/amUI/systemUser/SystemUserRequestPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ export const SystemUserRequestPage = () => {
166166
<div>
167167
{acceptCreationRequestError && (
168168
<DelegationCheckError
169+
accessPackages={request.accessPackages}
170+
resources={request.resources}
169171
defaultError='systemuser_includedrightspage.create_systemuser_error'
170172
error={acceptCreationRequestError as { data: ProblemDetail }}
171173
/>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
11
.delegationCheckError {
22
margin-top: var(--ds-size-4);
33
}
4+
5+
.delegationReasons {
6+
margin-top: var(--ds-size-2);
7+
font-size: var(--ds-font-size-2);
8+
white-space: pre-line;
9+
}
10+
11+
.delegationReasonsTitle {
12+
margin-top: var(--ds-size-2);
13+
font-weight: var(--ds-font-weight-medium);
14+
}

0 commit comments

Comments
 (0)