Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions src/Altinn.App.Core/Constants/General.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,9 @@ public static class General
/// Header name for platform access token
/// </summary>
internal const string PlatformAccessTokenHeaderName = "PlatformAccessToken";

/// <summary>
/// Header name for instance lock token
/// </summary>
internal const string LockTokenHeaderName = "Altinn-Storage-Lock-Token";
}
21 changes: 21 additions & 0 deletions src/Altinn.App.Core/Extensions/HttpClientExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
/// <param name="requestUri">The request Uri</param>
/// <param name="content">The http content</param>
/// <param name="platformAccessToken">The platformAccess tokens</param>
/// <param name="lockToken">The instance lock token</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>A HttpResponseMessage</returns>
public static async Task<HttpResponseMessage> PostAsync(
Expand All @@ -23,10 +24,11 @@
string requestUri,
HttpContent? content,
string? platformAccessToken = null,
string? lockToken = null,
CancellationToken cancellationToken = default
)
{
using HttpRequestMessage request = new(HttpMethod.Post, requestUri);

Check warning on line 31 in src/Altinn.App.Core/Extensions/HttpClientExtension.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this code to not construct the URL's path from user-controlled data.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ0AQnDVy9ImnsXwkMNF&open=AZ0AQnDVy9ImnsXwkMNF&pullRequest=1699
request.Content = content;

request.Headers.Authorization = new AuthenticationHeaderValue(
Expand All @@ -39,6 +41,11 @@
request.Headers.Add(Constants.General.PlatformAccessTokenHeaderName, platformAccessToken);
}

if (!string.IsNullOrEmpty(lockToken))
{
request.Headers.Add(Constants.General.LockTokenHeaderName, lockToken);
}

return await httpClient.SendAsync(request, cancellationToken);
}

Expand All @@ -50,6 +57,7 @@
/// <param name="requestUri">The request Uri</param>
/// <param name="content">The http content</param>
/// <param name="platformAccessToken">The platformAccess tokens</param>
/// <param name="lockToken">The instance lock token</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>A HttpResponseMessage</returns>
public static async Task<HttpResponseMessage> PutAsync(
Expand All @@ -58,6 +66,7 @@
string requestUri,
HttpContent? content,
string? platformAccessToken = null,
string? lockToken = null,
CancellationToken cancellationToken = default
)
{
Expand All @@ -74,6 +83,11 @@
request.Headers.Add(Constants.General.PlatformAccessTokenHeaderName, platformAccessToken);
}

if (!string.IsNullOrEmpty(lockToken))
{
request.Headers.Add(Constants.General.LockTokenHeaderName, lockToken);
}

return await httpClient.SendAsync(request, cancellationToken);
}

Expand All @@ -94,7 +108,7 @@
CancellationToken cancellationToken = default
)
{
using HttpRequestMessage request = new(HttpMethod.Get, requestUri);

Check warning on line 111 in src/Altinn.App.Core/Extensions/HttpClientExtension.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this code to not construct the URL's path from user-controlled data.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ0AQnDVy9ImnsXwkMNE&open=AZ0AQnDVy9ImnsXwkMNE&pullRequest=1699

request.Headers.Authorization = new AuthenticationHeaderValue(
Constants.AuthorizationSchemes.Bearer,
Expand Down Expand Up @@ -186,13 +200,15 @@
/// <param name="authorizationToken">the authorization token (jwt)</param>
/// <param name="requestUri">The request Uri</param>
/// <param name="platformAccessToken">The platformAccess tokens</param>
/// <param name="lockToken">The instance lock token</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>A HttpResponseMessage</returns>
public static async Task<HttpResponseMessage> DeleteAsync(
this HttpClient httpClient,
string authorizationToken,
string requestUri,
string? platformAccessToken = null,
string? lockToken = null,
CancellationToken cancellationToken = default
)
{
Expand All @@ -208,6 +224,11 @@
request.Headers.Add(Constants.General.PlatformAccessTokenHeaderName, platformAccessToken);
}

if (!string.IsNullOrEmpty(lockToken))
{
request.Headers.Add(Constants.General.LockTokenHeaderName, lockToken);
}

return await httpClient.SendAsync(request, cancellationToken);
}
}
4 changes: 2 additions & 2 deletions src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
services.AddHttpClient<IText, TextClient>();
#pragma warning restore CS0618 // Type or member is obsolete
services.AddHttpClient<IProcessClient, ProcessClient>();
services.AddHttpClient<InstanceLockClient>();
services.AddSingleton<InstanceLockClient>();
services.AddHttpClient<IPersonClient, PersonClient>();
services.AddHttpClient<IAccessManagementClient, AccessManagementClient>();

Expand Down Expand Up @@ -263,7 +263,7 @@
services.AddTransient<IEventHandlerResolver, EventHandlerResolver>();
services.TryAddSingleton<IEventSecretCodeProvider, KeyVaultEventSecretCodeProvider>();

// TODO: Event subs could be handled by the new automatic Maskinporten auth, once implemented.

Check warning on line 266 in src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this 'TODO' comment.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ0AQnE8y9ImnsXwkMNG&open=AZ0AQnE8y9ImnsXwkMNG&pullRequest=1699
// The event subscription client depends upon a Maskinporten message handler being
// added to the client during setup. As of now this needs to be done in the apps
// if subscription is to be added. This registration is to prevent the DI container
Expand Down Expand Up @@ -370,7 +370,7 @@
services.AddTransient<IAbandonTaskEventHandler, AbandonTaskEventHandler>();
services.AddTransient<IEndEventEventHandler, EndEventEventHandler>();

services.AddScoped<IInstanceLocker, InstanceLocker>();
services.AddSingleton<IInstanceLocker, InstanceLocker>();

// Process tasks
services.AddTransient<IProcessTask, DataProcessTask>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,14 @@ partial class Telemetry

return activity;
}

internal Activity? StartUpdateInstanceLockActivity(Guid instanceGuid, int instanceOwnerPartyId, TimeSpan ttl)
{
var activity = ActivitySource.StartActivity("UpdateInstanceLock");
activity?.SetInstanceId(instanceGuid);
activity?.SetInstanceOwnerPartyId(instanceOwnerPartyId);
activity?.SetTag("lock.ttl_seconds", (int)ttl.TotalSeconds);

return activity;
}
}
59 changes: 52 additions & 7 deletions src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Altinn.App.Core.Internal.App;
using Altinn.App.Core.Internal.Auth;
using Altinn.App.Core.Internal.Data;
using Altinn.App.Core.Internal.InstanceLocking;
using Altinn.App.Core.Models;
using Altinn.Platform.Storage.Interface.Models;
using Microsoft.AspNetCore.Http;
Expand All @@ -34,6 +35,7 @@
private readonly ModelSerializationService _modelSerializationService;
private readonly Telemetry? _telemetry;
private readonly HttpClient _client;
private readonly IInstanceLocker _instanceLocker;

private readonly AuthenticationMethod _defaultAuthenticationMethod = StorageAuthenticationMethod.CurrentUser();

Expand All @@ -52,6 +54,7 @@
_platformSettings = serviceProvider.GetRequiredService<IOptions<PlatformSettings>>().Value;
_logger = serviceProvider.GetRequiredService<ILogger<DataClient>>();
_telemetry = serviceProvider.GetService<Telemetry>();
_instanceLocker = serviceProvider.GetRequiredService<IInstanceLocker>();

httpClient.BaseAddress = new Uri(_platformSettings.ApiStorageEndpoint);
httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, _platformSettings.SubscriptionKey);
Expand All @@ -62,7 +65,7 @@
}

/// <inheritdoc />
[Obsolete("Use InsertFormData with Instance parameter instead")]

Check warning on line 68 in src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ0AQnCBy9ImnsXwkMM_&open=AZ0AQnCBy9ImnsXwkMM_&pullRequest=1699
public async Task<DataElement> InsertFormData<T>(
T dataToSerialize,
Guid instanceGuid,
Expand Down Expand Up @@ -116,7 +119,7 @@
}

/// <inheritdoc />
[Obsolete("Use the UpdateFormData method with Instance parameter instead")]

Check warning on line 122 in src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ0AQnCBy9ImnsXwkMNA&open=AZ0AQnCBy9ImnsXwkMNA&pullRequest=1699
public async Task<DataElement> UpdateData<T>(
T dataToSerialize,
Guid instanceGuid,
Expand Down Expand Up @@ -166,6 +169,7 @@
token,
apiUrl,
streamContent,
lockToken: _instanceLocker.CurrentLockToken,
cancellationToken: cts.Token
);

Expand Down Expand Up @@ -240,7 +244,7 @@

if (response.StatusCode == HttpStatusCode.NotFound)
{
// ! TODO: Remove null return in v9 and throw exception instead

Check warning on line 247 in src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this 'TODO' comment.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ0AQnCBy9ImnsXwkMM-&open=AZ0AQnCBy9ImnsXwkMM-&pullRequest=1699
return null!;
}

Expand Down Expand Up @@ -287,7 +291,7 @@
}

/// <inheritdoc />
[Obsolete("Use the overload with Instance parameter instead")]

Check warning on line 294 in src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ0AQnCBy9ImnsXwkMNB&open=AZ0AQnCBy9ImnsXwkMNB&pullRequest=1699
public async Task<object> GetFormData(
Guid instanceGuid,
Type type,
Expand Down Expand Up @@ -491,7 +495,12 @@
cancellationToken: cts.Token
);

HttpResponseMessage response = await _client.DeleteAsync(token, apiUrl, cancellationToken: cts.Token);
HttpResponseMessage response = await _client.DeleteAsync(
token,
apiUrl,
lockToken: _instanceLocker.CurrentLockToken,
cancellationToken: cts.Token
);

if (response.IsSuccessStatusCode)
{
Expand All @@ -505,7 +514,7 @@
}

/// <inheritdoc />
[Obsolete("The overload that takes a HttpRequest is deprecated, use the overload that takes a Stream instead")]

Check warning on line 517 in src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ0AQnCBy9ImnsXwkMNC&open=AZ0AQnCBy9ImnsXwkMNC&pullRequest=1699
public async Task<DataElement> InsertBinaryData(
string org,
string app,
Expand All @@ -529,7 +538,13 @@
);

StreamContent content = request.CreateContentStream();
HttpResponseMessage response = await _client.PostAsync(token, apiUrl, content, cancellationToken: cts.Token);
HttpResponseMessage response = await _client.PostAsync(
token,
apiUrl,
content,
lockToken: _instanceLocker.CurrentLockToken,
cancellationToken: cts.Token
);

if (response.IsSuccessStatusCode)
{
Expand Down Expand Up @@ -582,7 +597,13 @@
};
}

HttpResponseMessage response = await _client.PostAsync(token, apiUrl, content, cancellationToken: cts.Token);
HttpResponseMessage response = await _client.PostAsync(
token,
apiUrl,
content,
lockToken: _instanceLocker.CurrentLockToken,
cancellationToken: cts.Token
);

if (response.IsSuccessStatusCode)
{
Expand All @@ -603,7 +624,7 @@
}

/// <inheritdoc />
[Obsolete("The overload that takes a HttpRequest is deprecated, use the overload that takes a Stream instead")]

Check warning on line 627 in src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ0AQnCBy9ImnsXwkMND&open=AZ0AQnCBy9ImnsXwkMND&pullRequest=1699
public async Task<DataElement> UpdateBinaryData(
string org,
string app,
Expand All @@ -627,7 +648,13 @@

StreamContent content = request.CreateContentStream();

HttpResponseMessage response = await _client.PutAsync(token, apiUrl, content, cancellationToken: cts.Token);
HttpResponseMessage response = await _client.PutAsync(
token,
apiUrl,
content,
lockToken: _instanceLocker.CurrentLockToken,
cancellationToken: cts.Token
);

if (response.IsSuccessStatusCode)
{
Expand Down Expand Up @@ -676,7 +703,13 @@
};
}

HttpResponseMessage response = await _client.PutAsync(token, apiUrl, content, cancellationToken: cts.Token);
HttpResponseMessage response = await _client.PutAsync(
token,
apiUrl,
content,
lockToken: _instanceLocker.CurrentLockToken,
cancellationToken: cts.Token
);
_logger.LogInformation("Update binary data result: {ResultCode}", response.StatusCode);
if (response.IsSuccessStatusCode)
{
Expand Down Expand Up @@ -707,7 +740,13 @@
);

StringContent jsonString = new(JsonConvert.SerializeObject(dataElement), Encoding.UTF8, "application/json");
HttpResponseMessage response = await _client.PutAsync(token, apiUrl, jsonString, cancellationToken: cts.Token);
HttpResponseMessage response = await _client.PutAsync(
token,
apiUrl,
jsonString,
lockToken: _instanceLocker.CurrentLockToken,
cancellationToken: cts.Token
);

if (response.IsSuccessStatusCode)
{
Expand Down Expand Up @@ -750,6 +789,7 @@
apiUrl,
content: null,
platformAccessToken: null,
lockToken: _instanceLocker.CurrentLockToken,
cts.Token
);
if (response.IsSuccessStatusCode)
Expand Down Expand Up @@ -792,7 +832,12 @@
instanceIdentifier,
apiUrl
);
HttpResponseMessage response = await _client.DeleteAsync(token, apiUrl, cancellationToken: cts.Token);
HttpResponseMessage response = await _client.DeleteAsync(
token,
apiUrl,
lockToken: _instanceLocker.CurrentLockToken,
cancellationToken: cts.Token
);
if (response.IsSuccessStatusCode)
{
// ! TODO: this null-forgiving operator should be fixed/removed for the next major release
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,27 @@
using Altinn.App.Core.Helpers;
using Altinn.App.Core.Internal.Auth;
using Altinn.Platform.Storage.Interface.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Altinn.App.Core.Infrastructure.Clients.Storage;

internal sealed class InstanceLockClient
internal sealed class InstanceLockClient(
IOptionsMonitor<PlatformSettings> _platformSettings,
IAuthenticationTokenResolver _authenticationTokenResolver,
IHttpClientFactory _httpClientFactory,
Telemetry? _telemetry = null
)
{
private readonly ILogger<InstanceLockClient> _logger;
private readonly HttpClient _client;
private readonly Telemetry? _telemetry;
private readonly IAuthenticationTokenResolver _authenticationTokenResolver;

private readonly AuthenticationMethod _defaultAuthenticationMethod = StorageAuthenticationMethod.CurrentUser();

private const string LockTokenHeaderName = "Altinn-Storage-Lock-Token";

public InstanceLockClient(
IOptions<PlatformSettings> platformSettings,
ILogger<InstanceLockClient> logger,
IAuthenticationTokenResolver authenticationTokenResolver,
HttpClient httpClient,
Telemetry? telemetry = null
)
private HttpClient CreateHttpClient()
{
_logger = logger;
_authenticationTokenResolver = authenticationTokenResolver;
httpClient.BaseAddress = new Uri(platformSettings.Value.ApiStorageEndpoint);
httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey);
var settings = _platformSettings.CurrentValue;
var httpClient = _httpClientFactory.CreateClient();
httpClient.BaseAddress = new Uri(settings.ApiStorageEndpoint);
httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, settings.SubscriptionKey);
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_client = httpClient;
_telemetry = telemetry;
return httpClient;
}

public async Task<string> AcquireInstanceLock(
Expand All @@ -60,7 +50,8 @@ public async Task<string> AcquireInstanceLock(
var request = new InstanceLockRequest { TtlSeconds = (int)expiration.TotalSeconds };
var content = JsonContent.Create(request);

using var response = await _client.PostAsync(token, apiUrl, content, cancellationToken: cancellationToken);
using var client = CreateHttpClient();
using var response = await client.PostAsync(token, apiUrl, content, cancellationToken: cancellationToken);

if (!response.IsSuccessStatusCode)
{
Expand All @@ -77,7 +68,7 @@ public async Task<string> AcquireInstanceLock(
}
catch (Exception e) when (e is JsonException || e is InvalidOperationException)
{
_logger.LogError(e, "Error reading response from the lock acquisition endpoint.");
activity?.Errored(e, "Error reading response from the lock acquisition endpoint.");
}

if (string.IsNullOrEmpty(lockToken))
Expand All @@ -91,17 +82,18 @@ public async Task<string> AcquireInstanceLock(
return lockToken;
}

public async Task ReleaseInstanceLock(
public async Task UpdateInstanceLock(
Guid instanceGuid,
int instanceOwnerPartyId,
string lockToken,
TimeSpan ttl,
StorageAuthenticationMethod? authenticationMethod = null,
CancellationToken cancellationToken = default
)
{
using var activity = _telemetry?.StartReleaseInstanceLockActivity(instanceGuid, instanceOwnerPartyId);
using var activity = _telemetry?.StartUpdateInstanceLockActivity(instanceGuid, instanceOwnerPartyId, ttl);
string apiUrl = $"instances/{instanceOwnerPartyId}/{instanceGuid}/lock";
var instanceLockRequest = new InstanceLockRequest { TtlSeconds = 0 };
var instanceLockRequest = new InstanceLockRequest { TtlSeconds = (int)ttl.TotalSeconds };

var userToken = await _authenticationTokenResolver.GetAccessToken(
authenticationMethod ?? _defaultAuthenticationMethod,
Expand All @@ -111,9 +103,10 @@ public async Task ReleaseInstanceLock(
using HttpRequestMessage request = new(HttpMethod.Patch, apiUrl);
request.Content = JsonContent.Create(instanceLockRequest);
request.Headers.Authorization = new AuthenticationHeaderValue(AuthorizationSchemes.Bearer, userToken);
request.Headers.Add(LockTokenHeaderName, lockToken);
request.Headers.Add(General.LockTokenHeaderName, lockToken);

using var response = await _client.SendAsync(request, cancellationToken);
using var client = CreateHttpClient();
using var response = await client.SendAsync(request, cancellationToken);

if (!response.IsSuccessStatusCode)
{
Expand Down
8 changes: 8 additions & 0 deletions src/Altinn.App.Core/Internal/InstanceLocking/IInstanceLock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Altinn.App.Core.Internal.InstanceLocking;

internal interface IInstanceLock : IAsyncDisposable
{
Task Lock(TimeSpan? ttl = null);

Task UpdateTtl(TimeSpan ttl);
}
10 changes: 7 additions & 3 deletions src/Altinn.App.Core/Internal/InstanceLocking/IInstanceLocker.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
namespace Altinn.App.Core.Internal.InstanceLocking;

internal interface IInstanceLocker : IAsyncDisposable
internal interface IInstanceLocker
{
ValueTask LockAsync();
IInstanceLock InitLock();

ValueTask LockAsync(TimeSpan ttl);
Task<IInstanceLock> Lock();

Task<IInstanceLock> Lock(TimeSpan ttl);

string? CurrentLockToken { get; }
}
Loading
Loading