Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
2de4323
set up instance queries
adamhaeger Mar 17, 2026
fb06dd2
wip
adamhaeger Mar 17, 2026
64ab066
tests fixed
adamhaeger Mar 17, 2026
4baf12e
fixed tests
adamhaeger Mar 18, 2026
e1428e4
Merge branch 'main' into refactor/api-client-instance
adamhaeger Mar 18, 2026
09fa547
lint
adamhaeger Mar 18, 2026
16537ef
including full process from instance endpoint
adamhaeger Mar 18, 2026
799bd23
added fixture
adamhaeger Mar 18, 2026
907374d
updated verified files
adamhaeger Mar 18, 2026
9cbc6d9
Merge branch 'main' into refactor/api-client-instance
adamhaeger Mar 18, 2026
4dfede3
fixing verified files
adamhaeger Mar 18, 2026
c2e5157
fixes after code review
adamhaeger Mar 18, 2026
e583ad0
fixes after code review
adamhaeger Mar 18, 2026
55afd01
Merge branch 'main' into refactor/api-client-instance
adamhaeger Mar 18, 2026
c531062
frontend using full process from instance
adamhaeger Mar 18, 2026
0f68e53
bugfix
adamhaeger Mar 19, 2026
1e68738
Merge branch 'refactor/api-client-instance' into refactor/full-proces…
adamhaeger Mar 19, 2026
04693be
added HydrateFallback to prevent log errors
adamhaeger Mar 19, 2026
1fa54ba
test fix
adamhaeger Mar 19, 2026
b6b63a2
merge main
adamhaeger Mar 24, 2026
49ec78a
merge
adamhaeger Mar 24, 2026
af10ae2
removing irrelevant changes
adamhaeger Mar 24, 2026
bb66a31
fixed feedback test
adamhaeger Mar 25, 2026
b187bd1
merge main
adamhaeger Mar 25, 2026
d19b17c
reverted non intended changes
adamhaeger Mar 25, 2026
f043f56
reverted non intended changes
adamhaeger Mar 25, 2026
70b7f5a
reverted non intended changes
adamhaeger Mar 25, 2026
93db8e5
fixed on entry test
adamhaeger Mar 25, 2026
a4bd2c0
Merge branch 'main' into refactor/full-process-in-instance-2
adamhaeger Mar 26, 2026
872f61d
fixing tests
adamhaeger Mar 26, 2026
f6bd725
merge
adamhaeger Mar 26, 2026
9a8085c
removed unused file
adamhaeger Mar 26, 2026
cc1136e
merge
adamhaeger Mar 31, 2026
4949001
fix: add delay in GetSnapshotAppLogs to avoid flaky log capture on CI
adamhaeger Apr 1, 2026
a53d406
merge
adamhaeger Apr 1, 2026
2e95556
refactor: separated full instance into its own endpoint (#18390)
adamhaeger Apr 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public class InstancesController : ControllerBase
private readonly InstanceDataUnitOfWorkInitializer _instanceDataUnitOfWorkInitializer;
private readonly IAuthenticationContext _authenticationContext;
private readonly IDataElementAccessChecker _dataElementAccessChecker;
private readonly ProcessStateEnricher _processStateEnricher;
private const long RequestSizeLimit = 2000 * 1024 * 1024;

/// <summary>
Expand Down Expand Up @@ -127,6 +128,7 @@ IServiceProvider serviceProvider
_instanceDataUnitOfWorkInitializer = serviceProvider.GetRequiredService<InstanceDataUnitOfWorkInitializer>();
_authenticationContext = authenticationContext;
_dataElementAccessChecker = serviceProvider.GetRequiredService<IDataElementAccessChecker>();
_processStateEnricher = serviceProvider.GetRequiredService<ProcessStateEnricher>();
}

/// <summary>
Expand Down Expand Up @@ -202,6 +204,84 @@ await instance.WithOnlyAccessibleDataElements(_dataElementAccessChecker),
}
}

/// <summary>
/// Gets an instance object from storage with enriched process state including authorized actions,
/// read/write access, element types, and process task metadata.
/// </summary>
/// <param name="org">unique identifier of the organisation responsible for the app</param>
/// <param name="app">application identifier which is unique within an organisation</param>
/// <param name="instanceOwnerPartyId">unique id of the party that is the owner of the instance</param>
/// <param name="instanceGuid">unique id to identify the instance</param>
/// <param name="cancellationToken">cancellation token</param>
/// <returns>the instance with enriched process state</returns>
[Authorize]
[HttpGet("{instanceOwnerPartyId:int}/{instanceGuid:guid}/enriched")]
[Produces("application/json")]
[ProducesResponseType(typeof(EnrichedInstanceResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> GetEnriched(
[FromRoute] string org,
[FromRoute] string app,
[FromRoute] int instanceOwnerPartyId,
[FromRoute] Guid instanceGuid,
CancellationToken cancellationToken
)
{
EnforcementResult enforcementResult = await AuthorizeAction(
org,
app,
instanceOwnerPartyId,
instanceGuid,
"read"
);

if (!enforcementResult.Authorized)
{
return Forbidden(enforcementResult);
}

try
{
Instance instance = await _instanceClient.GetInstance(
app,
org,
instanceOwnerPartyId,
instanceGuid,
ct: cancellationToken
);
SelfLinkHelper.SetInstanceAppSelfLinks(instance, Request);

string? userOrgClaim = User.GetOrg();

if (userOrgClaim == null || !org.Equals(userOrgClaim, StringComparison.OrdinalIgnoreCase))
{
await _instanceClient.UpdateReadStatus(
instanceOwnerPartyId,
instanceGuid,
"read",
ct: cancellationToken
);
}

var instanceOwnerPartyTask = _registerClient.GetPartyUnchecked(instanceOwnerPartyId, cancellationToken);
var processStateTask = _processStateEnricher.Enrich(instance, instance.Process, User);

await Task.WhenAll(instanceOwnerPartyTask, processStateTask);

var dto = EnrichedInstanceResponse.From(
await instance.WithOnlyAccessibleDataElements(_dataElementAccessChecker),
await instanceOwnerPartyTask,
await processStateTask
);

return Ok(dto);
}
catch (Exception exception)
{
return ExceptionResponse(exception, $"Get enriched instance {instanceOwnerPartyId}/{instanceGuid} failed");
}
}

/// <summary>
/// Creates a new instance of an application in platform storage. Clients can send an instance as json or send a
/// multipart form-data with the instance in the first part named "instance" and the prefill data in the next parts, with
Expand Down
72 changes: 12 additions & 60 deletions src/App/backend/src/Altinn.App.Api/Controllers/ProcessController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,13 @@
using Altinn.App.Core.Internal.Data;
using Altinn.App.Core.Internal.Instances;
using Altinn.App.Core.Internal.Process;
using Altinn.App.Core.Internal.Process.Elements;
using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties;
using Altinn.App.Core.Internal.Validation;
using Altinn.App.Core.Models.Process;
using Altinn.App.Core.Models.Validation;
using Altinn.Platform.Storage.Interface.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using AppProcessState = Altinn.App.Core.Internal.Process.Elements.AppProcessState;
using IAuthorizationService = Altinn.App.Core.Internal.Auth.IAuthorizationService;

namespace Altinn.App.Api.Controllers;

Expand All @@ -34,12 +31,12 @@ public class ProcessController : ControllerBase
private readonly ILogger<ProcessController> _logger;
private readonly IInstanceClient _instanceClient;
private readonly IProcessClient _processClient;
private readonly IAuthorizationService _authorization;
private readonly IProcessEngine _processEngine;
private readonly IProcessReader _processReader;
private readonly IProcessEngineAuthorizer _processEngineAuthorizer;
private readonly IValidationService _validationService;
private readonly InstanceDataUnitOfWorkInitializer _instanceDataUnitOfWorkInitializer;
private readonly ProcessStateEnricher _processStateEnricher;

/// <summary>
/// Initializes a new instance of the <see cref="ProcessController"/>
Expand All @@ -49,22 +46,22 @@ public ProcessController(
IInstanceClient instanceClient,
IProcessClient processClient,
IValidationService validationService,
IAuthorizationService authorization,
IProcessReader processReader,
IProcessEngine processEngine,
IServiceProvider serviceProvider,
IProcessEngineAuthorizer processEngineAuthorizer
IProcessEngineAuthorizer processEngineAuthorizer,
ProcessStateEnricher processStateEnricher
)
{
_logger = logger;
_instanceClient = instanceClient;
_processClient = processClient;
_authorization = authorization;
_processReader = processReader;
_processEngine = processEngine;
_processEngineAuthorizer = processEngineAuthorizer;
_validationService = validationService;
_instanceDataUnitOfWorkInitializer = serviceProvider.GetRequiredService<InstanceDataUnitOfWorkInitializer>();
_processStateEnricher = processStateEnricher;
}

/// <summary>
Expand Down Expand Up @@ -96,7 +93,7 @@ [FromRoute] Guid instanceGuid
authenticationMethod: null,
CancellationToken.None
);
AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, instance.Process);
AppProcessState appProcessState = await _processStateEnricher.Enrich(instance, instance.Process, User);

return Ok(appProcessState);
}
Expand Down Expand Up @@ -163,9 +160,10 @@ public async Task<ActionResult<AppProcessState>> StartProcess(

await _processEngine.HandleEventsAndUpdateStorage(instance, null, result.ProcessStateChange?.Events);

AppProcessState appProcessState = await ConvertAndAuthorizeActions(
AppProcessState appProcessState = await _processStateEnricher.Enrich(
instance,
result.ProcessStateChange?.NewProcessState
result.ProcessStateChange?.NewProcessState,
User
);
Comment on lines +163 to 167
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check the ProcessChangeResult type to understand if ProcessStateChange can be null when Success is true

# Search for ProcessChangeResult class definition
ast-grep --pattern $'class ProcessChangeResult {
  $$$
}'

# Also check if there's nullability annotation on ProcessStateChange property
rg -n "ProcessStateChange" --type cs -C3 | head -80

Repository: Altinn/altinn-studio

Length of output: 7856


🏁 Script executed:

sed -n '140,160p' src/App/backend/src/Altinn.App.Api/Controllers/ProcessController.cs

Repository: Altinn/altinn-studio

Length of output: 895


🏁 Script executed:

sed -n '275,295p' src/App/backend/src/Altinn.App.Api/Controllers/ProcessController.cs

Repository: Altinn/altinn-studio

Length of output: 740


Remove unnecessary null-conditional operators from lines 147 and 151.

Both StartProcess and NextElement methods check result.Success before accessing ProcessStateChange, and the ProcessChangeResult type uses [MemberNotNullWhen(true, nameof(ProcessStateChange))] to guarantee non-null access when Success is true.

  • Line 147: result.ProcessStateChange?.Events should be result.ProcessStateChange.Events
  • Line 151: result.ProcessStateChange?.NewProcessState should be result.ProcessStateChange.NewProcessState

Line 287 correctly follows this pattern without the null-conditional operator. The current inconsistency obscures the actual contract and reduces code clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App/backend/src/Altinn.App.Api/Controllers/ProcessController.cs` around
lines 149 - 153, Remove the unnecessary null-conditional operators when
accessing ProcessChangeResult.ProcessStateChange in StartProcess and
NextElement: replace result.ProcessStateChange?.Events with
result.ProcessStateChange.Events and replace
result.ProcessStateChange?.NewProcessState with
result.ProcessStateChange.NewProcessState, because the method checks
result.Success and the type uses [MemberNotNullWhen(true,
nameof(ProcessStateChange))]; update the accesses in the StartProcess and
NextElement methods (and mirror the pattern used at the other occurrence) so the
code consistently reflects the non-null contract.

return Ok(appProcessState);
}
Expand Down Expand Up @@ -312,9 +310,10 @@ public async Task<ActionResult<AppProcessState>> NextElement(
return GetResultForError(result);
}

AppProcessState appProcessState = await ConvertAndAuthorizeActions(
AppProcessState appProcessState = await _processStateEnricher.Enrich(
instance,
result.ProcessStateChange.NewProcessState
result.ProcessStateChange.NewProcessState,
User
);

return Ok(appProcessState);
Expand Down Expand Up @@ -454,7 +453,7 @@ instance.Process.EndEvent is null
);
}

AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, instance.Process);
AppProcessState appProcessState = await _processStateEnricher.Enrich(instance, instance.Process, User);
return Ok(appProcessState);
}

Expand Down Expand Up @@ -498,48 +497,6 @@ await _processClient.GetProcessHistory(
}
}

private async Task<AppProcessState> ConvertAndAuthorizeActions(Instance instance, ProcessState? processState)
{
AppProcessState appProcessState = new AppProcessState(processState);
if (appProcessState.CurrentTask?.ElementId != null)
{
var flowElement = _processReader.GetFlowElement(appProcessState.CurrentTask.ElementId);
if (flowElement is ProcessTask processTask)
{
appProcessState.CurrentTask.Actions = new Dictionary<string, bool>();
List<AltinnAction> actions = new List<AltinnAction>() { new("read"), new("write") };
actions.AddRange(
processTask.ExtensionElements?.TaskExtension?.AltinnActions ?? new List<AltinnAction>()
);
var authDecisions = await AuthorizeActions(actions, instance);
appProcessState.CurrentTask.Actions = authDecisions
.Where(a => a.ActionType == ActionType.ProcessAction)
.ToDictionary(a => a.Id, a => a.Authorized);
appProcessState.CurrentTask.HasReadAccess = authDecisions.Single(a => a.Id == "read").Authorized;
appProcessState.CurrentTask.HasWriteAccess = authDecisions.Single(a => a.Id == "write").Authorized;
appProcessState.CurrentTask.UserActions = authDecisions;
appProcessState.CurrentTask.ElementType = flowElement.ElementType();
}
}

var processTasks = new List<AppProcessTaskTypeInfo>();
foreach (ProcessTask processElement in _processReader.GetAllFlowElements().OfType<ProcessTask>())
{
processTasks.Add(
new AppProcessTaskTypeInfo
{
ElementId = processElement.Id,
ElementType = processElement.ElementType(),
AltinnTaskType = processElement.ExtensionElements?.TaskExtension?.TaskType,
}
);
}

appProcessState.ProcessTasks = processTasks;

return appProcessState;
}

private ActionResult<AppProcessState> GetResultForError(ProcessChangeResult result)
{
switch (result.ErrorType)
Expand Down Expand Up @@ -676,11 +633,6 @@ private ObjectResult ExceptionResponse(Exception exception, string message)
return null;
}

private async Task<List<UserAction>> AuthorizeActions(List<AltinnAction> actions, Instance instance)
{
return await _authorization.AuthorizeActions(instance, HttpContext.User, actions);
}

private ActionResult HandlePlatformHttpException(PlatformHttpException e, string defaultMessage)
{
if (e.Response.StatusCode == HttpStatusCode.Forbidden)
Expand Down
Loading
Loading