diff --git a/backend/src/Designer/Configuration/Extensions/ServiceCollectionExtensions.cs b/backend/src/Designer/Configuration/Extensions/ServiceCollectionExtensions.cs index 9aead04b2df..0ef77f059d3 100644 --- a/backend/src/Designer/Configuration/Extensions/ServiceCollectionExtensions.cs +++ b/backend/src/Designer/Configuration/Extensions/ServiceCollectionExtensions.cs @@ -81,5 +81,18 @@ private static void ConfigureSettingsTypeBySection(this IServiceCollect services.TryAddScoped(typeof(TOption), svc => ((IOptionsSnapshot)svc.GetService(typeof(IOptionsSnapshot)))!.Value); } + public static IServiceCollection RegisterTransientServicesByBaseType(this IServiceCollection services) + { + var typesToRegister = AltinnAssembliesScanner.GetTypesAssignedFrom() + .Where(type => !type.IsInterface && !type.IsAbstract); + + foreach (var serviceType in typesToRegister) + { + services.AddTransient(typeof(TMarker), serviceType); + } + + return services; + } + } } diff --git a/backend/src/Designer/Controllers/RepositoryController.cs b/backend/src/Designer/Controllers/RepositoryController.cs index 2380f823e60..093394037f8 100644 --- a/backend/src/Designer/Controllers/RepositoryController.cs +++ b/backend/src/Designer/Controllers/RepositoryController.cs @@ -8,12 +8,10 @@ using Altinn.Studio.Designer.Configuration; using Altinn.Studio.Designer.Enums; using Altinn.Studio.Designer.Helpers; -using Altinn.Studio.Designer.Helpers.Extensions; using Altinn.Studio.Designer.Hubs.SyncHub; using Altinn.Studio.Designer.Models; using Altinn.Studio.Designer.RepositoryClient.Model; using Altinn.Studio.Designer.Services.Interfaces; -using Medallion.Threading; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -37,7 +35,6 @@ public class RepositoryController : ControllerBase private readonly ISourceControl _sourceControl; private readonly IRepository _repository; private readonly IHubContext _syncHub; - private readonly IDistributedLockProvider _synchronizationProvider; /// /// This is the API controller for functionality related to repositories. @@ -49,14 +46,12 @@ public class RepositoryController : ControllerBase /// the source control /// the repository control /// websocket syncHub - /// Provides distributed locks. - public RepositoryController(IGitea giteaWrapper, ISourceControl sourceControl, IRepository repository, IHubContext syncHub, IDistributedLockProvider synchronizationProvider) + public RepositoryController(IGitea giteaWrapper, ISourceControl sourceControl, IRepository repository, IHubContext syncHub) { _giteaApi = giteaWrapper; _sourceControl = sourceControl; _repository = repository; _syncHub = syncHub; - _synchronizationProvider = synchronizationProvider; } /// @@ -220,13 +215,8 @@ public async Task GetRepository(string org, string repository) [Route("repo/{org}/{repository:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}/status")] public async Task RepoStatus(string org, string repository) { - string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext); - await using (await _synchronizationProvider.AcquireLockAsync( - AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repository, developer))) - { - await _sourceControl.FetchRemoteChanges(org, repository); - return _sourceControl.RepositoryStatus(org, repository); - } + await _sourceControl.FetchRemoteChanges(org, repository); + return _sourceControl.RepositoryStatus(org, repository); } /// @@ -239,13 +229,8 @@ public async Task RepoStatus(string org, string repository) [Route("repo/{org}/{repository:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}/diff")] public async Task> RepoDiff(string org, string repository) { - string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext); - await using (await _synchronizationProvider.AcquireLockAsync( - AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repository, developer))) - { - await _sourceControl.FetchRemoteChanges(org, repository); - return await _sourceControl.GetChangedContent(org, repository); - } + await _sourceControl.FetchRemoteChanges(org, repository); + return await _sourceControl.GetChangedContent(org, repository); } /// @@ -258,22 +243,16 @@ public async Task> RepoDiff(string org, string reposi [Route("repo/{org}/{repository:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}/pull")] public async Task Pull(string org, string repository) { - string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext); - - await using (await _synchronizationProvider.AcquireLockAsync( - AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repository, developer))) - { - RepoStatus pullStatus = await _sourceControl.PullRemoteChanges(org, repository); + RepoStatus pullStatus = await _sourceControl.PullRemoteChanges(org, repository); - RepoStatus status = _sourceControl.RepositoryStatus(org, repository); - - if (pullStatus.RepositoryStatus != Enums.RepositoryStatus.Ok) - { - status.RepositoryStatus = pullStatus.RepositoryStatus; - } + RepoStatus status = _sourceControl.RepositoryStatus(org, repository); - return status; + if (pullStatus.RepositoryStatus != Enums.RepositoryStatus.Ok) + { + status.RepositoryStatus = pullStatus.RepositoryStatus; } + + return status; } /// @@ -289,18 +268,14 @@ public async Task ResetLocalRepository(string org, string reposito string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext); AltinnRepoEditingContext editingContext = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repository, developer); - await using (await _synchronizationProvider.AcquireLockAsync( - AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repository, developer))) + try { - try - { - await _repository.ResetLocalRepository(editingContext); - return Ok(); - } - catch (Exception) - { - return StatusCode(StatusCodes.Status500InternalServerError); - } + await _repository.ResetLocalRepository(editingContext); + return Ok(); + } + catch (Exception) + { + return StatusCode(StatusCodes.Status500InternalServerError); } } @@ -315,23 +290,19 @@ public async Task ResetLocalRepository(string org, string reposito public async Task CommitAndPushRepo(string org, string repository, [FromBody] CommitInfo commitInfo) { string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext); - await using (await _synchronizationProvider.AcquireLockAsync( - AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repository, developer))) + try { - try - { - await _sourceControl.PushChangesForRepository(commitInfo); - } - catch (LibGit2Sharp.NonFastForwardException) + await _sourceControl.PushChangesForRepository(commitInfo); + } + catch (LibGit2Sharp.NonFastForwardException) + { + RepoStatus repoStatus = await _sourceControl.PullRemoteChanges(commitInfo.Org, commitInfo.Repository); + await _sourceControl.Push(commitInfo.Org, commitInfo.Repository); + foreach (RepositoryContent repoContent in repoStatus?.ContentStatus) { - RepoStatus repoStatus = await _sourceControl.PullRemoteChanges(commitInfo.Org, commitInfo.Repository); - await _sourceControl.Push(commitInfo.Org, commitInfo.Repository); - foreach (RepositoryContent repoContent in repoStatus?.ContentStatus) - { - Source source = new(Path.GetFileName(repoContent.FilePath), repoContent.FilePath); - SyncSuccess syncSuccess = new(source); - await _syncHub.Clients.Group(developer).FileSyncSuccess(syncSuccess); - } + Source source = new(Path.GetFileName(repoContent.FilePath), repoContent.FilePath); + SyncSuccess syncSuccess = new(source); + await _syncHub.Clients.Group(developer).FileSyncSuccess(syncSuccess); } } } @@ -347,20 +318,15 @@ public async Task CommitAndPushRepo(string org, string repository, [FromBody] Co [Route("repo/{org}/{repository:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}/commit")] public async Task Commit(string org, string repository, [FromBody] CommitInfo commitInfo) { - string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext); - - await using (await _synchronizationProvider.AcquireLockAsync( - AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repository, developer))) + await Task.CompletedTask; + try { - try - { - _sourceControl.Commit(commitInfo); - return Ok(); - } - catch (Exception) - { - return StatusCode(StatusCodes.Status500InternalServerError); - } + _sourceControl.Commit(commitInfo); + return Ok(); + } + catch (Exception) + { + return StatusCode(StatusCodes.Status500InternalServerError); } } @@ -373,14 +339,8 @@ public async Task Commit(string org, string repository, [FromBody] [Route("repo/{org}/{repository:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}/push")] public async Task Push(string org, string repository) { - string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext); - - await using (await _synchronizationProvider.AcquireLockAsync( - AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repository, developer))) - { - bool pushSuccess = await _sourceControl.Push(org, repository); - return pushSuccess ? Ok() : StatusCode(StatusCodes.Status500InternalServerError); - } + bool pushSuccess = await _sourceControl.Push(org, repository); + return pushSuccess ? Ok() : StatusCode(StatusCodes.Status500InternalServerError); } /// diff --git a/backend/src/Designer/Middleware/UserRequestSynchronization/EditingContextResolver.cs b/backend/src/Designer/Middleware/UserRequestSynchronization/EditingContextResolver.cs new file mode 100644 index 00000000000..a6e70cbe744 --- /dev/null +++ b/backend/src/Designer/Middleware/UserRequestSynchronization/EditingContextResolver.cs @@ -0,0 +1,34 @@ +using Altinn.Studio.Designer.Helpers; +using Altinn.Studio.Designer.Models; +using Microsoft.AspNetCore.Http; + +namespace Altinn.Studio.Designer.Middleware.UserRequestSynchronization; + +public class EditingContextResolver : IEditingContextResolver +{ + public bool TryResolveContext(HttpContext httpContext, out AltinnRepoEditingContext context) + { + context = null; + var routeValues = httpContext.Request.RouteValues; + + string org = routeValues.TryGetValue("org", out var orgValue) ? orgValue?.ToString() : null; + string app = null; + + if (routeValues.TryGetValue("app", out object appValue) || routeValues.TryGetValue("repo", out appValue) || + routeValues.TryGetValue("repository", out appValue)) + { + app = appValue?.ToString(); + } + + string developer = AuthenticationHelper.GetDeveloperUserName(httpContext); + + + if (string.IsNullOrEmpty(org) || string.IsNullOrEmpty(app) || string.IsNullOrEmpty(developer)) + { + return false; + } + + context = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer); + return true; + } +} diff --git a/backend/src/Designer/Middleware/UserRequestSynchronization/IEditingContextResolver.cs b/backend/src/Designer/Middleware/UserRequestSynchronization/IEditingContextResolver.cs new file mode 100644 index 00000000000..d3943aec979 --- /dev/null +++ b/backend/src/Designer/Middleware/UserRequestSynchronization/IEditingContextResolver.cs @@ -0,0 +1,9 @@ +using Altinn.Studio.Designer.Models; +using Microsoft.AspNetCore.Http; + +namespace Altinn.Studio.Designer.Middleware.UserRequestSynchronization; + +public interface IEditingContextResolver +{ + bool TryResolveContext(HttpContext httpContext, out AltinnRepoEditingContext context); +} diff --git a/backend/src/Designer/Middleware/UserRequestSynchronization/IRequestSyncEvaluator.cs b/backend/src/Designer/Middleware/UserRequestSynchronization/IRequestSyncEvaluator.cs new file mode 100644 index 00000000000..18881606980 --- /dev/null +++ b/backend/src/Designer/Middleware/UserRequestSynchronization/IRequestSyncEvaluator.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Http; + +namespace Altinn.Studio.Designer.Middleware.UserRequestSynchronization; + +public interface IRequestSyncEvaluator +{ + bool EvaluateSyncRequest(HttpContext httpContext); +} diff --git a/backend/src/Designer/Middleware/UserRequestSynchronization/IRequestSyncResolver.cs b/backend/src/Designer/Middleware/UserRequestSynchronization/IRequestSyncResolver.cs new file mode 100644 index 00000000000..5a73979231b --- /dev/null +++ b/backend/src/Designer/Middleware/UserRequestSynchronization/IRequestSyncResolver.cs @@ -0,0 +1,9 @@ +using Altinn.Studio.Designer.Models; +using Microsoft.AspNetCore.Http; + +namespace Altinn.Studio.Designer.Middleware.UserRequestSynchronization; + +public interface IRequestSyncResolver +{ + bool TryResolveSyncRequest(HttpContext httpContext, out AltinnRepoEditingContext editingContext); +} diff --git a/backend/src/Designer/Middleware/UserRequestSynchronization/RequestSyncEvaluatorImplementations/RepositoryEndpointsSyncEvaluator.cs b/backend/src/Designer/Middleware/UserRequestSynchronization/RequestSyncEvaluatorImplementations/RepositoryEndpointsSyncEvaluator.cs new file mode 100644 index 00000000000..dc7c5d0a9ea --- /dev/null +++ b/backend/src/Designer/Middleware/UserRequestSynchronization/RequestSyncEvaluatorImplementations/RepositoryEndpointsSyncEvaluator.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using Altinn.Studio.Designer.Controllers; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Controllers; + +namespace Altinn.Studio.Designer.Middleware.UserRequestSynchronization.RequestSyncEvaluatorImplementations; + +public class RepositoryEndpointsSyncEvaluator : IRequestSyncEvaluator +{ + private readonly string _controllerName = nameof(RepositoryController).Replace("Controller", string.Empty); + private readonly List _actionNamesWhiteList = + [ + nameof(RepositoryController.RepoStatus), + nameof(RepositoryController.RepoDiff), + nameof(RepositoryController.Pull), + nameof(RepositoryController.ResetLocalRepository), + nameof(RepositoryController.CommitAndPushRepo), + nameof(RepositoryController.Commit), + nameof(RepositoryController.Push) + ]; + + public bool EvaluateSyncRequest(HttpContext httpContext) + { + var endpoint = httpContext.GetEndpoint(); + + var controllerActionDescriptor = endpoint?.Metadata.GetMetadata(); + + if (controllerActionDescriptor == null) + { + return false; + } + + string controllerName = controllerActionDescriptor.ControllerName; + string actionName = controllerActionDescriptor.ActionName; + if (controllerName.Equals(_controllerName, StringComparison.OrdinalIgnoreCase) && _actionNamesWhiteList.Contains(actionName)) + { + return true; + } + + return false; + } +} diff --git a/backend/src/Designer/Middleware/UserRequestSynchronization/RequestSyncExtensions.cs b/backend/src/Designer/Middleware/UserRequestSynchronization/RequestSyncExtensions.cs new file mode 100644 index 00000000000..76246397ff4 --- /dev/null +++ b/backend/src/Designer/Middleware/UserRequestSynchronization/RequestSyncExtensions.cs @@ -0,0 +1,16 @@ +using Altinn.Studio.Designer.Configuration.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Altinn.Studio.Designer.Middleware.UserRequestSynchronization; + +public static class RequestSyncExtensions +{ + public static IServiceCollection RegisterSynchronizationServices(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + services.RegisterTransientServicesByBaseType(); + return services; + } + +} diff --git a/backend/src/Designer/Middleware/UserRequestSynchronization/RequestSyncResolver.cs b/backend/src/Designer/Middleware/UserRequestSynchronization/RequestSyncResolver.cs new file mode 100644 index 00000000000..12521cb6816 --- /dev/null +++ b/backend/src/Designer/Middleware/UserRequestSynchronization/RequestSyncResolver.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Linq; +using Altinn.Studio.Designer.Models; +using Microsoft.AspNetCore.Http; + +namespace Altinn.Studio.Designer.Middleware.UserRequestSynchronization; + +public class RequestSyncResolver : IRequestSyncResolver +{ + IEnumerable _requestSyncEvaluators; + IEditingContextResolver _editingContextResolver; + + public RequestSyncResolver(IEnumerable requestSyncEvaluators, IEditingContextResolver editingContextResolver) + { + _requestSyncEvaluators = requestSyncEvaluators; + _editingContextResolver = editingContextResolver; + } + + public bool TryResolveSyncRequest(HttpContext httpContext, out AltinnRepoEditingContext editingContext) + { + if (!_editingContextResolver.TryResolveContext(httpContext, out editingContext)) + { + return false; + } + + return _requestSyncEvaluators.Any(e => e.EvaluateSyncRequest(httpContext)); + } +} diff --git a/backend/src/Designer/Middleware/UserRequestSynchronization/RequestSynchronizationMiddleware.cs b/backend/src/Designer/Middleware/UserRequestSynchronization/RequestSynchronizationMiddleware.cs new file mode 100644 index 00000000000..8efc327f67a --- /dev/null +++ b/backend/src/Designer/Middleware/UserRequestSynchronization/RequestSynchronizationMiddleware.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using Altinn.Studio.Designer.Helpers.Extensions; +using Altinn.Studio.Designer.Models; +using Medallion.Threading; +using Microsoft.AspNetCore.Http; + +namespace Altinn.Studio.Designer.Middleware.UserRequestSynchronization; + +public class RequestSynchronizationMiddleware +{ + private readonly RequestDelegate _next; + + public RequestSynchronizationMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext httpContext, IRequestSyncResolver requestSyncResolver, IDistributedLockProvider synchronizationProvider) + { + if (requestSyncResolver.TryResolveSyncRequest(httpContext, out AltinnRepoEditingContext editingContext)) + { + await using (await synchronizationProvider.AcquireLockAsync(editingContext)) + { + await _next(httpContext); + return; + } + } + + await _next(httpContext); + } +} diff --git a/backend/src/Designer/Program.cs b/backend/src/Designer/Program.cs index bd8c6a9dbb8..5007ea8421e 100644 --- a/backend/src/Designer/Program.cs +++ b/backend/src/Designer/Program.cs @@ -14,6 +14,7 @@ using Altinn.Studio.Designer.Infrastructure; using Altinn.Studio.Designer.Infrastructure.AnsattPorten; using Altinn.Studio.Designer.Infrastructure.Authorization; +using Altinn.Studio.Designer.Middleware.UserRequestSynchronization; using Altinn.Studio.Designer.Services.Implementation; using Altinn.Studio.Designer.Services.Interfaces; using Altinn.Studio.Designer.Tracing; @@ -261,6 +262,7 @@ void ConfigureServices(IServiceCollection services, IConfiguration configuration services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly)); services.AddTransient(); services.AddFeatureManagement(); + services.RegisterSynchronizationServices(); if (!env.IsDevelopment()) @@ -334,6 +336,8 @@ void Configure(IConfiguration configuration) app.MapHub("/previewHub"); app.MapHub("/sync-hub"); + app.UseMiddleware(); + logger.LogInformation("// Program.cs // Configure // Configuration complete"); }