From d817d7ccaf9b6e99b2d68b3fcb3f6b82d8b8a9c8 Mon Sep 17 00:00:00 2001 From: MariusVuscanNx <96233009+MariusVuscanNx@users.noreply.github.com> Date: Mon, 27 May 2024 11:06:39 +0300 Subject: [PATCH] Global read-only and read-only workflows (#5406) * Implemented global readonly and workflow readonly * Fixed bug of unfilled versions * Fixed failing test * Updated pipeline * Revert * Pushed fixes based on the PR comments and fixed history bug * Implemented changes based on PR comments * Fixed issue in caused by AuthorizationHandler not being able to receive null resources * Remove unused Google.Protobuf dependency in Program.cs An unused Protobuf library import was found in Program.cs file. In order to clean up the code and improve readability, this import statement has been removed. No other changes have been made. * Refactor code to reduce redundancy and improve readability Optimize code by removing unnecessary constructor assignments and simplify function calls. The simplification is mainly achieved by directly passing the required services as constructor parameters instead of assigning them to private fields first. This results in a more readable and less redundant code. --------- Co-authored-by: Sipke Schoorstra --- src/bundles/Elsa.Server.Web/Program.cs | 13 +- .../Models/WorkflowDefinition.cs | 6 +- .../Models/WorkflowDefinitionModel.cs | 3 +- .../Models/WorkflowDefinitionSummary.cs | 14 +- .../Elsa.Api.Client/Shared/Models/Link.cs | 3 + .../Shared/Models/LinkedEntity.cs | 12 ++ .../Shared/Models/PagedListResponse.cs | 6 +- .../Elsa.Api.Common/Models/LinkedResource.cs | 8 + .../Models/PagedListResponse.cs | 2 +- .../Stores/DapperWorkflowDefinitionStore.cs | 2 + .../Management/WorkflowDefinitionStore.cs | 1 + .../Constants/AuthorizationPolicies.cs | 9 + .../Contracts/IWorkflowDefinitionLinker.cs | 19 ++ .../Elsa.Workflows.Api.csproj | 10 +- .../ActivityDescriptorOptions/Get/Models.cs | 9 +- .../ActivityDescriptors/List/Endpoint.cs | 11 +- .../ActivityDescriptors/List/Models.cs | 9 +- .../IncidentStrategies/List/Endpoint.cs | 11 +- .../ExpressionDescriptors/List/Endpoint.cs | 11 +- .../Endpoints/StorageDrivers/List/Models.cs | 9 +- .../Endpoints/VariableTypes/List/Models.cs | 9 +- .../List/Endpoint.cs | 11 +- .../BulkDelete/Endpoint.cs | 24 +-- .../WorkflowDefinitions/BulkDelete/Models.cs | 9 +- .../BulkDeleteVersions/Endpoint.cs | 23 ++- .../BulkDeleteVersions/Models.cs | 9 +- .../BulkDispatch/Endpoint.cs | 20 +-- .../BulkPublish/Endpoint.cs | 45 +++-- .../WorkflowDefinitions/BulkPublish/Models.cs | 16 +- .../BulkRetract/Endpoint.cs | 36 ++-- .../WorkflowDefinitions/BulkRetract/Models.cs | 19 +- .../WorkflowDefinitions/Count/Endpoint.cs | 11 +- .../WorkflowDefinitions/Delete/Endpoint.cs | 39 ++++- .../DeleteVersion/Endpoint.cs | 23 ++- .../WorkflowDefinitions/Execute/Endpoint.cs | 1 - .../WorkflowDefinitions/Execute/Models.cs | 9 +- .../GetByDefinitionId/Endpoint.cs | 24 +-- .../WorkflowDefinitions/GetById/Endpoint.cs | 21 +-- .../GetManyById/Endpoint.cs | 26 +-- .../WorkflowDefinitions/Import/Endpoint.cs | 31 ++-- .../ImportFiles/Endpoint.cs | 24 ++- .../IsNameUnique/Endpoint.cs | 11 +- .../WorkflowDefinitions/List/Endpoint.cs | 8 +- .../WorkflowDefinitions/Post/Endpoint.cs | 80 +++++---- .../WorkflowDefinitions/Publish/Endpoint.cs | 38 ++-- .../WorkflowDefinitions/Retract/Endpoint.cs | 36 ++-- .../UpdateReferences/Endpoint.cs | 27 +-- .../WorkflowDefinitions/Version/Delete.cs | 48 ++++- .../WorkflowDefinitions/Version/List.cs | 12 +- .../WorkflowDefinitions/Version/Revert.cs | 46 +++-- .../WorkflowInstances/BulkCancel/Models.cs | 9 +- .../WorkflowInstances/BulkDelete/Endpoint.cs | 11 +- .../WorkflowInstances/BulkDelete/Models.cs | 9 +- .../WorkflowInstances/Delete/Endpoint.cs | 7 +- .../WorkflowInstances/Get/Endpoint.cs | 11 +- .../Journal/FilteredList/Models.cs | 12 +- .../WorkflowInstances/Journal/List/Models.cs | 12 +- .../Features/WorkflowsApiFeature.cs | 13 ++ ...JsonSerializationErrorHandlerMiddleware.cs | 9 +- .../Models/LinkedWorkflowDefinitionModel.cs | 6 + .../Models/LinkedWorkflowDefinitionSummary.cs | 9 + .../Requirements/NotReadOnlyRequirement.cs | 43 +++++ .../ArgumentJsonConverterFactory.cs | 11 +- .../SerializationConfigurator.cs | 11 +- .../StaticWorkflowDefinitionLinker.cs | 165 ++++++++++++++++++ .../Contracts/IWorkflowDefinitionManager.cs | 10 +- .../Elsa.Workflows.Management.csproj | 1 + .../Features/WorkflowManagementFeature.cs | 13 ++ .../Filters/WorkflowDefinitionFilter.cs | 6 + .../Mappers/WorkflowDefinitionMapper.cs | 6 +- .../Models/WorkflowDefinitionSummary.cs | 7 +- .../Options/ManagementOptions.cs | 7 +- .../Services/WorkflowDefinitionManager.cs | 47 ++--- .../Helpers/Fixtures/WorkflowServer.cs | 2 + 74 files changed, 812 insertions(+), 529 deletions(-) create mode 100644 src/clients/Elsa.Api.Client/Shared/Models/Link.cs create mode 100644 src/clients/Elsa.Api.Client/Shared/Models/LinkedEntity.cs create mode 100644 src/common/Elsa.Api.Common/Models/LinkedResource.cs create mode 100644 src/modules/Elsa.Workflows.Api/Constants/AuthorizationPolicies.cs create mode 100644 src/modules/Elsa.Workflows.Api/Contracts/IWorkflowDefinitionLinker.cs create mode 100644 src/modules/Elsa.Workflows.Api/Models/LinkedWorkflowDefinitionModel.cs create mode 100644 src/modules/Elsa.Workflows.Api/Models/LinkedWorkflowDefinitionSummary.cs create mode 100644 src/modules/Elsa.Workflows.Api/Requirements/NotReadOnlyRequirement.cs create mode 100644 src/modules/Elsa.Workflows.Api/Services/StaticWorkflowDefinitionLinker.cs diff --git a/src/bundles/Elsa.Server.Web/Program.cs b/src/bundles/Elsa.Server.Web/Program.cs index c08402b27..2eeb158b7 100644 --- a/src/bundles/Elsa.Server.Web/Program.cs +++ b/src/bundles/Elsa.Server.Web/Program.cs @@ -41,12 +41,13 @@ const bool useDapper = false; const bool useProtoActor = false; const bool useHangfire = false; const bool useQuartz = true; -const bool useMassTransit = true; +const bool useMassTransit = false; const bool useZipCompression = true; const bool runEFCoreMigrations = true; const bool useMemoryStores = false; -const bool useCaching = true; -const DistributedCachingTransport distributedCachingTransport = DistributedCachingTransport.MassTransit; +const bool useCaching = false; +const bool useReadOnlyMode = false; +const DistributedCachingTransport distributedCachingTransport = DistributedCachingTransport.None; const MassTransitBroker useMassTransitBroker = MassTransitBroker.Memory; var builder = WebApplication.CreateBuilder(args); @@ -160,6 +161,7 @@ services management.UseCache(); management.SetDefaultLogPersistenceMode(LogPersistenceMode.Inherit); + management.UseReadOnlyMode(useReadOnlyMode); }) .UseWorkflowRuntime(runtime => { @@ -242,7 +244,10 @@ services if (useQuartz) scheduling.UseQuartzScheduler(); }) - .UseWorkflowsApi(api => api.AddFastEndpointsAssembly()) + .UseWorkflowsApi(api => + { + api.AddFastEndpointsAssembly(); + }) .UseRealTimeWorkflows() .UseCSharp(options => { diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinition.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinition.cs index f7a78ba21..f3fe5d0eb 100644 --- a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinition.cs +++ b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinition.cs @@ -4,9 +4,9 @@ using Elsa.Api.Client.Shared.Models; namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models; /// -/// Represents a versioned workflow definition. +/// Represents a linked workflow definition. /// -public class WorkflowDefinition : VersionedEntity +public class WorkflowDefinition : LinkedEntity { /// /// The logical ID of the workflow. This ID is the same across versions. @@ -77,7 +77,7 @@ public class WorkflowDefinition : VersionedEntity /// The root activity of the workflow. /// public JsonObject Root { get; set; } = default!; - + /// /// An option to use the workflow as a readonly workflow. /// diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinitionModel.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinitionModel.cs index 4985f802e..b0b840d37 100644 --- a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinitionModel.cs +++ b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinitionModel.cs @@ -1,4 +1,5 @@ using System.Text.Json.Nodes; +using Elsa.Api.Client.Shared.Models; using JetBrains.Annotations; namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models; @@ -7,7 +8,7 @@ namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models; /// Represents a serializable workflow definition. /// [PublicAPI] -public class WorkflowDefinitionModel +public class WorkflowDefinitionModel : LinkedEntity { /// /// Gets or sets the version ID of the workflow definition. diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinitionSummary.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinitionSummary.cs index 715a2e52a..54bc6ecde 100644 --- a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinitionSummary.cs +++ b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinitionSummary.cs @@ -1,3 +1,4 @@ +using Elsa.Api.Client.Shared.Models; using JetBrains.Annotations; namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models; @@ -6,4 +7,15 @@ namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models; /// A summary of a workflow definition. /// [PublicAPI] -public record WorkflowDefinitionSummary(string Id, string DefinitionId, string Name, string? Description, int Version, bool IsLatest, bool IsPublished, string MaterializerName, DateTimeOffset CreatedAt); \ No newline at end of file +public class WorkflowDefinitionSummary : LinkedEntity +{ + public string Id { get; set; } + public string DefinitionId { get; set; } + public string Name { get; set; } + public string? Description { get; set; } + public int Version { get; set; } + public bool IsLatest { get; set; } + public bool IsPublished { get; set; } + public string MaterializerName { get; set; } + public DateTimeOffset CreatedAt { get; set; } +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Shared/Models/Link.cs b/src/clients/Elsa.Api.Client/Shared/Models/Link.cs new file mode 100644 index 000000000..aae33d99b --- /dev/null +++ b/src/clients/Elsa.Api.Client/Shared/Models/Link.cs @@ -0,0 +1,3 @@ +namespace Elsa.Api.Client.Shared.Models; + +public record Link(string Href, string Rel, string Method); diff --git a/src/clients/Elsa.Api.Client/Shared/Models/LinkedEntity.cs b/src/clients/Elsa.Api.Client/Shared/Models/LinkedEntity.cs new file mode 100644 index 000000000..306ddd26a --- /dev/null +++ b/src/clients/Elsa.Api.Client/Shared/Models/LinkedEntity.cs @@ -0,0 +1,12 @@ +namespace Elsa.Api.Client.Shared.Models; + +/// +/// Represents an entity that is linked, meaning it contains links used in the context of HATEOAS. +/// +public class LinkedEntity : VersionedEntity +{ + /// + /// A list of links that with the possible actions used in the context of HATEOAS. + /// + public Link[]? Links { get; set; } = default; +} diff --git a/src/clients/Elsa.Api.Client/Shared/Models/PagedListResponse.cs b/src/clients/Elsa.Api.Client/Shared/Models/PagedListResponse.cs index 9df005a6f..14866e631 100644 --- a/src/clients/Elsa.Api.Client/Shared/Models/PagedListResponse.cs +++ b/src/clients/Elsa.Api.Client/Shared/Models/PagedListResponse.cs @@ -6,4 +6,8 @@ namespace Elsa.Api.Client.Shared.Models; /// A page of items. /// The total number of items. /// The type of the items. -public record PagedListResponse(ICollection Items, long TotalCount); \ No newline at end of file +public class PagedListResponse : LinkedEntity +{ + public ICollection Items { get; set; } + public long TotalCount { get; set; } +} \ No newline at end of file diff --git a/src/common/Elsa.Api.Common/Models/LinkedResource.cs b/src/common/Elsa.Api.Common/Models/LinkedResource.cs new file mode 100644 index 000000000..b5b9aa7a4 --- /dev/null +++ b/src/common/Elsa.Api.Common/Models/LinkedResource.cs @@ -0,0 +1,8 @@ +namespace Elsa.Models; + +public record LinkedResource(Link[] Links) +{ + public LinkedResource() : this([]) { } +} + +public record Link(string Href, string Rel, string Method); \ No newline at end of file diff --git a/src/common/Elsa.Api.Common/Models/PagedListResponse.cs b/src/common/Elsa.Api.Common/Models/PagedListResponse.cs index 16c9f7146..07bfebe9e 100644 --- a/src/common/Elsa.Api.Common/Models/PagedListResponse.cs +++ b/src/common/Elsa.Api.Common/Models/PagedListResponse.cs @@ -2,7 +2,7 @@ using Elsa.Common.Models; namespace Elsa.Models; -public class PagedListResponse +public record PagedListResponse: LinkedResource { public PagedListResponse() { diff --git a/src/modules/Elsa.Dapper/Modules/Management/Stores/DapperWorkflowDefinitionStore.cs b/src/modules/Elsa.Dapper/Modules/Management/Stores/DapperWorkflowDefinitionStore.cs index 3c86fe8c6..e1d454cb9 100644 --- a/src/modules/Elsa.Dapper/Modules/Management/Stores/DapperWorkflowDefinitionStore.cs +++ b/src/modules/Elsa.Dapper/Modules/Management/Stores/DapperWorkflowDefinitionStore.cs @@ -176,6 +176,8 @@ public class DapperWorkflowDefinitionStore : IWorkflowDefinitionStore .Is(nameof(WorkflowDefinition.Name), filter.Name) .In(nameof(WorkflowDefinition.Name), filter.Names) .Is(nameof(WorkflowDefinition.Options.UsableAsActivity), filter.UsableAsActivity) + .Is(nameof(WorkflowDefinition.IsSystem), filter.IsSystem) + .Is(nameof(WorkflowDefinition.IsReadonly), filter.IsReadonly) .WorkflowDefinitionSearchTerm(filter.SearchTerm); } diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs index 7fd0f8985..fcd7efa94 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs @@ -194,6 +194,7 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore if (filter.UsableAsActivity != null) queryable = queryable.Where(x => EF.Property(x, "UsableAsActivity") == filter.UsableAsActivity); if (!string.IsNullOrWhiteSpace(filter.SearchTerm)) queryable = queryable.Where(x => x.Name!.Contains(filter.SearchTerm) || x.Description!.Contains(filter.SearchTerm) || x.Id.Contains(filter.SearchTerm) || x.DefinitionId.Contains(filter.SearchTerm)); if (filter.IsSystem != null) queryable = queryable.Where(x => x.IsSystem == filter.IsSystem); + if (filter.IsReadonly != null) queryable = queryable.Where(x => x.IsReadonly == filter.IsReadonly); return queryable; } diff --git a/src/modules/Elsa.Workflows.Api/Constants/AuthorizationPolicies.cs b/src/modules/Elsa.Workflows.Api/Constants/AuthorizationPolicies.cs new file mode 100644 index 000000000..179886f0b --- /dev/null +++ b/src/modules/Elsa.Workflows.Api/Constants/AuthorizationPolicies.cs @@ -0,0 +1,9 @@ +namespace Elsa.Workflows.Api.Constants; + +public static class AuthorizationPolicies +{ + /// + /// The read-only policy ensures that that workflow management is not possible when the read-only mode is enabled or when a workflow is marked as read-only. + /// + public const string NotReadOnlyPolicy = "NotReadOnlyPolicy"; +} diff --git a/src/modules/Elsa.Workflows.Api/Contracts/IWorkflowDefinitionLinker.cs b/src/modules/Elsa.Workflows.Api/Contracts/IWorkflowDefinitionLinker.cs new file mode 100644 index 000000000..1712479c7 --- /dev/null +++ b/src/modules/Elsa.Workflows.Api/Contracts/IWorkflowDefinitionLinker.cs @@ -0,0 +1,19 @@ +using Elsa.Models; +using Elsa.Workflows.Api.Models; +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Management.Models; + +namespace Elsa.Workflows.Api; + +/// Maps workflow definition models to liked models +public interface IWorkflowDefinitionLinker +{ + /// Maps to an enhanced model that contains links with the possible operations applicable to a workflow definition. + Task MapAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default); + + /// Maps a paged list to an enhanced model that contains links with the possible operations applicable to a workflow definition. + PagedListResponse MapAsync(PagedListResponse list, CancellationToken cancellationToken = default); + + /// Maps a list to an enhanced model that contains links with the possible operations applicable to a workflow definition. + Task> MapAsync(List definitions, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Elsa.Workflows.Api.csproj b/src/modules/Elsa.Workflows.Api/Elsa.Workflows.Api.csproj index 3dd82eb24..4fff95dc9 100644 --- a/src/modules/Elsa.Workflows.Api/Elsa.Workflows.Api.csproj +++ b/src/modules/Elsa.Workflows.Api/Elsa.Workflows.Api.csproj @@ -8,11 +8,11 @@ - - - - - + + + + + diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/ActivityDescriptorOptions/Get/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/ActivityDescriptorOptions/Get/Models.cs index 8f9c08408..4b05094ff 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/ActivityDescriptorOptions/Get/Models.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/ActivityDescriptorOptions/Get/Models.cs @@ -13,12 +13,7 @@ internal class Request } [PublicAPI] -internal class Response +internal class Response(IDictionary items) { - public Response(IDictionary items) - { - Items = items; - } - - public IDictionary Items { get; set; } + public IDictionary Items { get; set; } = items; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/ActivityDescriptors/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/ActivityDescriptors/List/Endpoint.cs index 909c6e80a..321e1e286 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/ActivityDescriptors/List/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/ActivityDescriptors/List/Endpoint.cs @@ -5,15 +5,8 @@ using JetBrains.Annotations; namespace Elsa.Workflows.Api.Endpoints.ActivityDescriptors.List; [PublicAPI] -internal class List : ElsaEndpointWithoutRequest +internal class List(IActivityRegistry registry) : ElsaEndpointWithoutRequest { - private readonly IActivityRegistry _registry; - - public List(IActivityRegistry registry) - { - _registry = registry; - } - public override void Configure() { Get("/descriptors/activities"); @@ -22,7 +15,7 @@ internal class List : ElsaEndpointWithoutRequest public override Task ExecuteAsync(CancellationToken cancellationToken) { - var descriptors = _registry.ListAll().ToList(); + var descriptors = registry.ListAll().ToList(); var response = new Response(descriptors); return Task.FromResult(response); diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/ActivityDescriptors/List/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/ActivityDescriptors/List/Models.cs index 619fdfcdd..bae0bfc91 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/ActivityDescriptors/List/Models.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/ActivityDescriptors/List/Models.cs @@ -4,13 +4,8 @@ using JetBrains.Annotations; namespace Elsa.Workflows.Api.Endpoints.ActivityDescriptors.List; [PublicAPI] -internal class Response +internal class Response(ICollection items) { - public Response(ICollection items) - { - Items = items; - } - - public ICollection Items { get; set; } + public ICollection Items { get; set; } = items; public int Count => Items.Count; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/IncidentStrategies/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/IncidentStrategies/List/Endpoint.cs index 5af20b051..463199b75 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/IncidentStrategies/List/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/IncidentStrategies/List/Endpoint.cs @@ -12,15 +12,8 @@ namespace Elsa.Workflows.Api.Endpoints.IncidentStrategies.List; /// /// Returns list of available implementations. /// -internal class List : ElsaEndpointWithoutRequest> +internal class List(IEnumerable strategies) : ElsaEndpointWithoutRequest> { - private readonly IEnumerable _strategies; - - public List(IEnumerable strategies) - { - _strategies = strategies; - } - public override void Configure() { Get("/descriptors/incident-strategies"); @@ -29,7 +22,7 @@ internal class List : ElsaEndpointWithoutRequest> ExecuteAsync(CancellationToken cancellationToken) { - var descriptors = _strategies.Select(IncidentStrategyDescriptor.FromStrategy).OrderBy(x => x.DisplayName).ToList(); + var descriptors = strategies.Select(IncidentStrategyDescriptor.FromStrategy).OrderBy(x => x.DisplayName).ToList(); var response =new ListResponse(descriptors); return Task.FromResult(response); } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/Scripting/ExpressionDescriptors/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/Scripting/ExpressionDescriptors/List/Endpoint.cs index 89e46fb15..c0aed0303 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/Scripting/ExpressionDescriptors/List/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/Scripting/ExpressionDescriptors/List/Endpoint.cs @@ -10,15 +10,8 @@ namespace Elsa.Workflows.Api.Endpoints.Scripting.ExpressionDescriptors.List; /// Returns a TypeScript definition that is used by the Monaco editor to display intellisense for JavaScript expressions. /// [UsedImplicitly] -internal class List : ElsaEndpointWithoutRequest> +internal class List(IExpressionDescriptorRegistry expressionDescriptorRegistry) : ElsaEndpointWithoutRequest> { - private readonly IExpressionDescriptorRegistry _expressionDescriptorRegistry; - - public List(IExpressionDescriptorRegistry expressionDescriptorRegistry) - { - _expressionDescriptorRegistry = expressionDescriptorRegistry; - } - /// public override void Configure() { @@ -29,7 +22,7 @@ internal class List : ElsaEndpointWithoutRequest public override Task HandleAsync(CancellationToken cancellationToken) { - var descriptors = _expressionDescriptorRegistry.ListAll().ToList(); + var descriptors = expressionDescriptorRegistry.ListAll().ToList(); var models = Map(descriptors).ToList(); var response = new ListResponse(models); return SendOkAsync(response, cancellationToken); diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/StorageDrivers/List/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/StorageDrivers/List/Models.cs index d2b92623f..127a1ce68 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/StorageDrivers/List/Models.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/StorageDrivers/List/Models.cs @@ -1,13 +1,8 @@ namespace Elsa.Workflows.Api.Endpoints.StorageDrivers.List; -public class Response +public class Response(ICollection items) { - public Response(ICollection items) - { - Items = items; - } - - public ICollection Items { get; set; } + public ICollection Items { get; set; } = items; } public record StorageDriverDescriptor(string TypeName, string DisplayName); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/VariableTypes/List/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/VariableTypes/List/Models.cs index 44e3c8ad6..4aad7314f 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/VariableTypes/List/Models.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/VariableTypes/List/Models.cs @@ -1,13 +1,8 @@ namespace Elsa.Workflows.Api.Endpoints.VariableTypes.List; -internal class Response +internal class Response(ICollection items) { - public Response(ICollection items) - { - Items = items; - } - - public ICollection Items { get; set; } + public ICollection Items { get; set; } = items; } internal record VariableTypeDescriptor(string TypeName, string DisplayName, string Category, string? Description); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowActivationStrategies/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowActivationStrategies/List/Endpoint.cs index 70cc8fa48..1162eae1e 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowActivationStrategies/List/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowActivationStrategies/List/Endpoint.cs @@ -12,15 +12,8 @@ namespace Elsa.Workflows.Api.Endpoints.WorkflowActivationStrategies.List; /// /// Returns list of available implementations. /// -internal class List : ElsaEndpointWithoutRequest> +internal class List(IEnumerable strategies) : ElsaEndpointWithoutRequest> { - private readonly IEnumerable _strategies; - - public List(IEnumerable strategies) - { - _strategies = strategies; - } - public override void Configure() { Get("/descriptors/workflow-activation-strategies"); @@ -29,7 +22,7 @@ internal class List : ElsaEndpointWithoutRequest> ExecuteAsync(CancellationToken cancellationToken) { - var descriptors = _strategies.Select(WorkflowActivationStrategyDescriptor.FromStrategy).OrderBy(x => x.DisplayName).ToList(); + var descriptors = strategies.Select(WorkflowActivationStrategyDescriptor.FromStrategy).OrderBy(x => x.DisplayName).ToList(); var response =new ListResponse(descriptors); return Task.FromResult(response); } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDelete/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDelete/Endpoint.cs index c7756c25e..2a8ff142c 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDelete/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDelete/Endpoint.cs @@ -1,19 +1,15 @@ using Elsa.Abstractions; +using Elsa.Workflows.Api.Constants; using Elsa.Workflows.Management.Contracts; using JetBrains.Annotations; - +using Microsoft.AspNetCore.Authorization; +using Elsa.Workflows.Api.Requirements; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkDelete; [UsedImplicitly] -internal class BulkDelete : ElsaEndpoint +internal class BulkDelete(IWorkflowDefinitionManager workflowDefinitionManager, IAuthorizationService authorizationService) + : ElsaEndpoint { - private readonly IWorkflowDefinitionManager _workflowDefinitionManager; - - public BulkDelete(IWorkflowDefinitionManager workflowDefinitionManager) - { - _workflowDefinitionManager = workflowDefinitionManager; - } - public override void Configure() { Post("/bulk-actions/delete/workflow-definitions/by-definition-id"); @@ -22,7 +18,15 @@ internal class BulkDelete : ElsaEndpoint public override async Task ExecuteAsync(Request request, CancellationToken cancellationToken) { - var count = await _workflowDefinitionManager.BulkDeleteByDefinitionIdsAsync(request.DefinitionIds, cancellationToken); + var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy); + + if (!authorizationResult.Result.Succeeded) + { + await SendForbiddenAsync(cancellationToken); + return null!; + } + + var count = await workflowDefinitionManager.BulkDeleteByDefinitionIdsAsync(request.DefinitionIds, cancellationToken); return new Response(count); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDelete/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDelete/Models.cs index b95faec0a..41fd3c32b 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDelete/Models.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDelete/Models.cs @@ -7,12 +7,7 @@ internal class Request public ICollection DefinitionIds { get; set; } = default!; } -internal class Response +internal class Response(long deletedCount) { - public Response(long deletedCount) - { - DeletedCount = deletedCount; - } - - [JsonPropertyName("deleted")] public long DeletedCount { get; } + [JsonPropertyName("deleted")] public long DeletedCount { get; } = deletedCount; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDeleteVersions/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDeleteVersions/Endpoint.cs index 2d563ce1d..fc0d3a95b 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDeleteVersions/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDeleteVersions/Endpoint.cs @@ -1,19 +1,16 @@ using Elsa.Abstractions; +using Elsa.Workflows.Api.Constants; +using Elsa.Workflows.Api.Requirements; using Elsa.Workflows.Management.Contracts; using JetBrains.Annotations; +using Microsoft.AspNetCore.Authorization; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkDeleteVersions; [PublicAPI] -internal class BulkDeleteVersions : ElsaEndpoint +internal class BulkDeleteVersions(IWorkflowDefinitionManager workflowDefinitionManager, IAuthorizationService authorizationService) + : ElsaEndpoint { - private readonly IWorkflowDefinitionManager _workflowDefinitionManager; - - public BulkDeleteVersions(IWorkflowDefinitionManager workflowDefinitionManager) - { - _workflowDefinitionManager = workflowDefinitionManager; - } - public override void Configure() { Post("/bulk-actions/delete/workflow-definitions/by-id"); @@ -22,7 +19,15 @@ internal class BulkDeleteVersions : ElsaEndpoint public override async Task ExecuteAsync(Request request, CancellationToken cancellationToken) { - var count = await _workflowDefinitionManager.BulkDeleteByIdsAsync(request.Ids, cancellationToken); + var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy); + + if (!authorizationResult.Result.Succeeded) + { + await SendForbiddenAsync(cancellationToken); + return null!; + } + + var count = await workflowDefinitionManager.BulkDeleteByIdsAsync(request.Ids, cancellationToken); return new Response(count); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDeleteVersions/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDeleteVersions/Models.cs index e0bbcf359..1f3933d4f 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDeleteVersions/Models.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDeleteVersions/Models.cs @@ -7,12 +7,7 @@ internal class Request public ICollection Ids { get; set; } = default!; } -internal class Response +internal class Response(long deletedCount) { - public Response(long deletedCount) - { - DeletedCount = deletedCount; - } - - [JsonPropertyName("deleted")] public long DeletedCount { get; } + [JsonPropertyName("deleted")] public long DeletedCount { get; } = deletedCount; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDispatch/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDispatch/Endpoint.cs index c269a6d33..ea3e80ce3 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDispatch/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDispatch/Endpoint.cs @@ -10,19 +10,9 @@ using JetBrains.Annotations; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkDispatch; [PublicAPI] -internal class Endpoint : ElsaEndpoint +internal class Endpoint(IWorkflowDefinitionStore store, IWorkflowDispatcher workflowDispatcher, IIdentityGenerator identityGenerator) + : ElsaEndpoint { - private readonly IWorkflowDefinitionStore _store; - private readonly IWorkflowDispatcher _workflowDispatcher; - private readonly IIdentityGenerator _identityGenerator; - - public Endpoint(IWorkflowDefinitionStore store, IWorkflowDispatcher workflowDispatcher, IIdentityGenerator identityGenerator) - { - _store = store; - _workflowDispatcher = workflowDispatcher; - _identityGenerator = identityGenerator; - } - public override void Configure() { Post("/workflow-definitions/{definitionId}/bulk-dispatch"); @@ -34,7 +24,7 @@ internal class Endpoint : ElsaEndpoint var definitionId = request.DefinitionId; var versionOptions = request.VersionOptions ?? VersionOptions.Published; - var exists = await _store.AnyAsync( + var exists = await store.AnyAsync( new WorkflowDefinitionFilter { DefinitionId = definitionId, @@ -52,7 +42,7 @@ internal class Endpoint : ElsaEndpoint for (var i = 0; i < request.Count; i++) { - var instanceId = _identityGenerator.GenerateId(); + var instanceId = identityGenerator.GenerateId(); var triggerActivityId = request.TriggerActivityId; var input = (IDictionary?)request.Input; var dispatchRequest = new DispatchWorkflowDefinitionRequest @@ -64,7 +54,7 @@ internal class Endpoint : ElsaEndpoint TriggerActivityId = triggerActivityId }; - await _workflowDispatcher.DispatchAsync(dispatchRequest, cancellationToken: cancellationToken); + await workflowDispatcher.DispatchAsync(dispatchRequest, cancellationToken: cancellationToken); instanceIds.Add(instanceId); } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs index af605dda1..9e12132a6 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs @@ -1,23 +1,18 @@ using Elsa.Abstractions; using Elsa.Common.Models; +using Elsa.Workflows.Api.Constants; +using Elsa.Workflows.Api.Requirements; using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Management.Filters; using JetBrains.Annotations; +using Microsoft.AspNetCore.Authorization; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkPublish; [PublicAPI] -internal class BulkPublish : ElsaEndpoint +internal class BulkPublish(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher, IAuthorizationService authorizationService) + : ElsaEndpoint { - private readonly IWorkflowDefinitionStore _store; - private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher; - - public BulkPublish(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher) - { - _store = store; - _workflowDefinitionPublisher = workflowDefinitionPublisher; - } - public override void Configure() { Post("/bulk-actions/publish/workflow-definitions/by-definition-ids"); @@ -26,20 +21,30 @@ internal class BulkPublish : ElsaEndpoint public override async Task ExecuteAsync(Request request, CancellationToken cancellationToken) { + var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy); + + if (!authorizationResult.Result.Succeeded) + { + await SendForbiddenAsync(cancellationToken); + return null!; + } + var published = new List(); var notFound = new List(); var alreadyPublished = new List(); + var skipped = new List(); - var definitions = (await _store.FindManyAsync(new WorkflowDefinitionFilter - { - DefinitionIds = request.DefinitionIds, VersionOptions = VersionOptions.Latest - }, cancellationToken: cancellationToken)) + var definitions = (await store.FindManyAsync(new WorkflowDefinitionFilter + { + DefinitionIds = request.DefinitionIds, + VersionOptions = VersionOptions.Latest + }, cancellationToken: cancellationToken)) .DistinctBy(x => x.DefinitionId) .ToDictionary(x => x.DefinitionId); foreach (var definitionId in request.DefinitionIds) { - if(!definitions.TryGetValue(definitionId, out var definition)) + if (!definitions.TryGetValue(definitionId, out var definition)) { notFound.Add(definitionId); continue; @@ -51,10 +56,16 @@ internal class BulkPublish : ElsaEndpoint continue; } - await _workflowDefinitionPublisher.PublishAsync(definition, cancellationToken); + if (definition.IsReadonly) + { + skipped.Add(definitionId); + continue; + } + + await workflowDefinitionPublisher.PublishAsync(definition, cancellationToken); published.Add(definitionId); } - return new Response(published, alreadyPublished, notFound); + return new Response(published, alreadyPublished, notFound, skipped); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Models.cs index 8ffa665e5..0cb2e8f01 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Models.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Models.cs @@ -5,16 +5,10 @@ internal class Request public ICollection DefinitionIds { get; set; } = default!; } -internal class Response +internal class Response(ICollection published, ICollection alreadyPublished, ICollection notFound, ICollection skipped) { - public Response(ICollection published, ICollection alreadyPublished, ICollection notFound) - { - Published = published; - AlreadyPublished = alreadyPublished; - NotFound = notFound; - } - - public ICollection Published { get; } - public ICollection AlreadyPublished { get; } - public ICollection NotFound { get; } + public ICollection Published { get; } = published; + public ICollection AlreadyPublished { get; } = alreadyPublished; + public ICollection NotFound { get; } = notFound; + public ICollection Skipped { get; } = skipped; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkRetract/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkRetract/Endpoint.cs index 602351878..7cbba9d45 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkRetract/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkRetract/Endpoint.cs @@ -1,23 +1,18 @@ using Elsa.Abstractions; using Elsa.Common.Models; +using Elsa.Workflows.Api.Constants; +using Elsa.Workflows.Api.Requirements; using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Management.Filters; using JetBrains.Annotations; +using Microsoft.AspNetCore.Authorization; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkRetract; [PublicAPI] -internal class BulkRetract : ElsaEndpoint +internal class BulkRetract(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher, IAuthorizationService authorizationService) + : ElsaEndpoint { - private readonly IWorkflowDefinitionStore _store; - private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher; - - public BulkRetract(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher) - { - _store = store; - _workflowDefinitionPublisher = workflowDefinitionPublisher; - } - public override void Configure() { Post("/bulk-actions/retract/workflow-definitions/by-definition-ids"); @@ -26,13 +21,22 @@ internal class BulkRetract : ElsaEndpoint public override async Task ExecuteAsync(Request request, CancellationToken cancellationToken) { + var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy); + + if (!authorizationResult.Result.Succeeded) + { + await SendForbiddenAsync(cancellationToken); + return null!; + } + var retracted = new List(); var notFound = new List(); var notPublished = new List(); + var skipped = new List(); foreach (var definitionId in request.DefinitionIds) { - var definitions = (await _store.FindManyAsync(new WorkflowDefinitionFilter + var definitions = (await store.FindManyAsync(new WorkflowDefinitionFilter { DefinitionId = definitionId, VersionOptions = VersionOptions.LatestOrPublished @@ -51,10 +55,16 @@ internal class BulkRetract : ElsaEndpoint continue; } - await _workflowDefinitionPublisher.RetractAsync(published, cancellationToken); + if (published.IsReadonly) + { + skipped.Add(definitionId); + continue; + } + + await workflowDefinitionPublisher.RetractAsync(published, cancellationToken); retracted.Add(definitionId); } - return new Response(retracted, notPublished, notFound); + return new Response(retracted, notPublished, notFound, skipped); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkRetract/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkRetract/Models.cs index 307b7e2ef..283d5c0e1 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkRetract/Models.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkRetract/Models.cs @@ -5,21 +5,14 @@ internal class Request public ICollection DefinitionIds { get; set; } = default!; } -internal class Response +internal class Response(ICollection retracted, ICollection alreadyRetracted, ICollection notFound, ICollection skipped) { - public Response(ICollection retracted, ICollection alreadyRetracted, ICollection notFound) - { - Retracted = retracted; - AlreadyRetracted = alreadyRetracted; - NotPublished = alreadyRetracted; - NotFound = notFound; - } - - public ICollection Retracted { get; } - public ICollection AlreadyRetracted { get; } + public ICollection Retracted { get; } = retracted; + public ICollection AlreadyRetracted { get; } = alreadyRetracted; + public ICollection Skipped { get; } = skipped; [Obsolete("Use AlreadyRetracted instead")] - public ICollection NotPublished { get; } + public ICollection NotPublished { get; } = alreadyRetracted; - public ICollection NotFound { get; } + public ICollection NotFound { get; } = notFound; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Count/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Count/Endpoint.cs index ebf14b02f..a5a078db9 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Count/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Count/Endpoint.cs @@ -8,15 +8,8 @@ namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Count; /// An endpoint for counting workflow definitions. /// [PublicAPI] -internal class Count : ElsaEndpointWithoutRequest +internal class Count(IWorkflowDefinitionStore store) : ElsaEndpointWithoutRequest { - private readonly IWorkflowDefinitionStore _store; - - public Count(IWorkflowDefinitionStore store) - { - _store = store; - } - public override void Configure() { Get("/workflow-definitions/query/count"); @@ -25,7 +18,7 @@ internal class Count : ElsaEndpointWithoutRequest public override async Task HandleAsync(CancellationToken cancellationToken) { - var count = await _store.CountDistinctAsync(cancellationToken); + var count = await store.CountDistinctAsync(cancellationToken); var response = new Response(count); await SendOkAsync(response, cancellationToken); } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Delete/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Delete/Endpoint.cs index 72741136d..1f801a609 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Delete/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Delete/Endpoint.cs @@ -1,17 +1,16 @@ using Elsa.Abstractions; +using Elsa.Common.Models; +using Elsa.Workflows.Api.Constants; +using Elsa.Workflows.Api.Requirements; using Elsa.Workflows.Management.Contracts; +using Elsa.Workflows.Management.Filters; +using Microsoft.AspNetCore.Authorization; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Delete; -internal class Delete : ElsaEndpoint +internal class Delete(IWorkflowDefinitionManager workflowDefinitionManager, IAuthorizationService authorizationService, IWorkflowDefinitionStore store) + : ElsaEndpoint { - private readonly IWorkflowDefinitionManager _workflowDefinitionManager; - - public Delete(IWorkflowDefinitionManager workflowDefinitionManager) - { - _workflowDefinitionManager = workflowDefinitionManager; - } - public override void Configure() { Delete("/workflow-definitions/{definitionId}"); @@ -20,7 +19,29 @@ internal class Delete : ElsaEndpoint public override async Task HandleAsync(Request request, CancellationToken cancellationToken) { - var result = await _workflowDefinitionManager.DeleteByDefinitionIdAsync(request.DefinitionId, cancellationToken); + var filter = new WorkflowDefinitionFilter + { + DefinitionId = request.DefinitionId, + VersionOptions = VersionOptions.Latest + }; + + var definition = await store.FindAsync(filter, cancellationToken); + + if (definition == null) + { + await SendNotFoundAsync(cancellationToken); + return; + } + + var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); + + if (!authorizationResult.Result.Succeeded) + { + await SendForbiddenAsync(cancellationToken); + return; + } + + var result = await workflowDefinitionManager.DeleteByDefinitionIdAsync(request.DefinitionId, cancellationToken); if (result == 0) await SendNotFoundAsync(cancellationToken); diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/DeleteVersion/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/DeleteVersion/Endpoint.cs index 3eb832d06..c45be498d 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/DeleteVersion/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/DeleteVersion/Endpoint.cs @@ -1,19 +1,16 @@ using Elsa.Abstractions; +using Elsa.Workflows.Api.Constants; +using Elsa.Workflows.Api.Requirements; using Elsa.Workflows.Management.Contracts; using JetBrains.Annotations; +using Microsoft.AspNetCore.Authorization; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.DeleteVersion; [PublicAPI] -internal class DeleteVersion : ElsaEndpoint +internal class DeleteVersion(IWorkflowDefinitionManager workflowDefinitionManager, IAuthorizationService authorizationService) + : ElsaEndpoint { - private readonly IWorkflowDefinitionManager _workflowDefinitionManager; - - public DeleteVersion(IWorkflowDefinitionManager workflowDefinitionManager) - { - _workflowDefinitionManager = workflowDefinitionManager; - } - public override void Configure() { Delete("/workflow-definition-versions/{id}"); @@ -22,7 +19,15 @@ internal class DeleteVersion : ElsaEndpoint public override async Task HandleAsync(Request request, CancellationToken cancellationToken) { - var deleted = await _workflowDefinitionManager.DeleteByIdAsync(request.Id, cancellationToken); + var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy); + + if (!authorizationResult.Result.Succeeded) + { + await SendForbiddenAsync(cancellationToken); + return; + } + + var deleted = await workflowDefinitionManager.DeleteByIdAsync(request.Id, cancellationToken); if (!deleted) await SendNotFoundAsync(cancellationToken); diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/Endpoint.cs index 78460bce6..da91f61f1 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/Endpoint.cs @@ -6,7 +6,6 @@ using Elsa.Workflows.Contracts; using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Management.Filters; using Elsa.Workflows.Runtime.Contracts; -using Elsa.Workflows.Runtime.Options; using Elsa.Workflows.Runtime.Parameters; using Elsa.Workflows.State; using JetBrains.Annotations; diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/Models.cs index 24aabdfe2..5ff14414e 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/Models.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/Models.cs @@ -17,12 +17,7 @@ public class Request public object? Input { get; set; } } -public class Response +public class Response(WorkflowState workflowState) { - public Response(WorkflowState workflowState) - { - WorkflowState = workflowState; - } - - public WorkflowState WorkflowState { get; } + public WorkflowState WorkflowState { get; } = workflowState; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/GetByDefinitionId/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/GetByDefinitionId/Endpoint.cs index 2cd3ee905..3b0fa767a 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/GetByDefinitionId/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/GetByDefinitionId/Endpoint.cs @@ -5,7 +5,6 @@ using Elsa.Extensions; using Elsa.Workflows.Contracts; using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Management.Filters; -using Elsa.Workflows.Management.Mappers; using Elsa.Workflows.Serialization.Converters; using JetBrains.Annotations; using Microsoft.AspNetCore.Http; @@ -13,19 +12,9 @@ using Microsoft.AspNetCore.Http; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.GetByDefinitionId; [PublicAPI] -internal class GetByDefinitionId : ElsaEndpoint +internal class GetByDefinitionId(IWorkflowDefinitionStore store, IApiSerializer apiSerializer, IWorkflowDefinitionLinker linker) + : ElsaEndpoint { - private readonly IWorkflowDefinitionStore _store; - private readonly IApiSerializer _apiSerializer; - private readonly WorkflowDefinitionMapper _mapper; - - public GetByDefinitionId(IWorkflowDefinitionStore store, IApiSerializer apiSerializer, WorkflowDefinitionMapper mapper) - { - _store = store; - _apiSerializer = apiSerializer; - _mapper = mapper; - } - public override void Configure() { Get("/workflow-definitions/by-definition-id/{definitionId}", "/workflow-definitions/{definitionId}"); @@ -43,16 +32,17 @@ internal class GetByDefinitionId : ElsaEndpoint }; var order = new WorkflowDefinitionOrder(x => x.Version, OrderDirection.Descending); - var definition = (await _store.FindManyAsync(filter, order, cancellationToken: cancellationToken)).FirstOrDefault(); + var definition = (await store.FindManyAsync(filter, order, cancellationToken: cancellationToken)).FirstOrDefault(); if (definition == null) { await SendNotFoundAsync(cancellationToken); return; } - - var model = await _mapper.MapAsync(definition, cancellationToken); - var serializerOptions = _apiSerializer.GetOptions(); + + var model = await linker.MapAsync(definition, cancellationToken); + + var serializerOptions = apiSerializer.GetOptions(); // If the root of composite activities is not requested, exclude them from being serialized. if (!request.IncludeCompositeRoot) diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/GetById/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/GetById/Endpoint.cs index 47852fb48..9a299932c 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/GetById/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/GetById/Endpoint.cs @@ -3,7 +3,6 @@ using Elsa.Extensions; using Elsa.Workflows.Contracts; using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Management.Filters; -using Elsa.Workflows.Management.Mappers; using Elsa.Workflows.Serialization.Converters; using JetBrains.Annotations; using Microsoft.AspNetCore.Http; @@ -11,19 +10,9 @@ using Microsoft.AspNetCore.Http; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.GetById; [PublicAPI] -internal class GetById : ElsaEndpoint +internal class GetById(IWorkflowDefinitionStore store, IApiSerializer apiSerializer, IWorkflowDefinitionLinker linker) + : ElsaEndpoint { - private readonly IWorkflowDefinitionStore _store; - private readonly IApiSerializer _apiSerializer; - private readonly WorkflowDefinitionMapper _mapper; - - public GetById(IWorkflowDefinitionStore store, IApiSerializer apiSerializer, WorkflowDefinitionMapper mapper) - { - _store = store; - _apiSerializer = apiSerializer; - _mapper = mapper; - } - public override void Configure() { Get("/workflow-definitions/by-id/{id}"); @@ -37,7 +26,7 @@ internal class GetById : ElsaEndpoint Id = request.Id }; - var definition = await _store.FindAsync(filter, cancellationToken); + var definition = await store.FindAsync(filter, cancellationToken); if (definition == null) { @@ -45,8 +34,8 @@ internal class GetById : ElsaEndpoint return; } - var model = await _mapper.MapAsync(definition, cancellationToken); - var serializerOptions = _apiSerializer.GetOptions().Clone(); + var model = await linker.MapAsync(definition, cancellationToken); + var serializerOptions = apiSerializer.GetOptions().Clone(); // If the root of composite activities is not requested, exclude them from being serialized. if (!request.IncludeCompositeRoot) diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/GetManyById/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/GetManyById/Endpoint.cs index 7b56f9eb4..082497f81 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/GetManyById/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/GetManyById/Endpoint.cs @@ -1,11 +1,10 @@ using Elsa.Abstractions; using Elsa.Extensions; using Elsa.Models; +using Elsa.Workflows.Api.Models; using Elsa.Workflows.Contracts; using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Management.Filters; -using Elsa.Workflows.Management.Mappers; -using Elsa.Workflows.Management.Models; using Elsa.Workflows.Serialization.Converters; using JetBrains.Annotations; using Microsoft.AspNetCore.Http; @@ -13,19 +12,9 @@ using Microsoft.AspNetCore.Http; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.GetManyById; [PublicAPI] -internal class GetManyById : ElsaEndpoint +internal class GetManyById(IWorkflowDefinitionStore store, IApiSerializer apiSerializer, IWorkflowDefinitionLinker linker) + : ElsaEndpoint { - private readonly IWorkflowDefinitionStore _store; - private readonly IApiSerializer _apiSerializer; - private readonly WorkflowDefinitionMapper _mapper; - - public GetManyById(IWorkflowDefinitionStore store, IApiSerializer apiSerializer, WorkflowDefinitionMapper mapper) - { - _store = store; - _apiSerializer = apiSerializer; - _mapper = mapper; - } - public override void Configure() { Get("/workflow-definitions/many-by-id"); @@ -39,15 +28,16 @@ internal class GetManyById : ElsaEndpoint Ids = request.Ids }; - var definitions = (await _store.FindManyAsync(filter, cancellationToken)).ToList(); - var models = (await _mapper.MapAsync(definitions, cancellationToken)).ToList(); - var serializerOptions = _apiSerializer.GetOptions().Clone(); + var definitions = (await store.FindManyAsync(filter, cancellationToken)).ToList(); + var serializerOptions = apiSerializer.GetOptions().Clone(); // If the root of composite activities is not requested, exclude them from being serialized. if (!request.IncludeCompositeRoot) serializerOptions.Converters.Add(new JsonIgnoreCompositeRootConverterFactory()); - var response = new ListResponse(models); + var models = await linker.MapAsync(definitions, cancellationToken); + var response = new ListResponse(models); + await HttpContext.Response.WriteAsJsonAsync(response, serializerOptions, cancellationToken); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs index 055e72fe6..33869e840 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs @@ -1,9 +1,10 @@ using Elsa.Abstractions; -using Elsa.Workflows.Contracts; +using Elsa.Workflows.Api.Constants; +using Elsa.Workflows.Api.Requirements; using Elsa.Workflows.Management.Contracts; -using Elsa.Workflows.Management.Mappers; using Elsa.Workflows.Management.Models; using JetBrains.Annotations; +using Microsoft.AspNetCore.Authorization; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Import; @@ -13,22 +14,19 @@ namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Import; [PublicAPI] internal class Import : ElsaEndpoint { - private readonly IWorkflowDefinitionService _workflowDefinitionService; private readonly IWorkflowDefinitionImporter _workflowDefinitionImporter; - private readonly WorkflowDefinitionMapper _workflowDefinitionMapper; - private readonly IApiSerializer _apiSerializer; + private readonly IWorkflowDefinitionLinker _linker; + private readonly IAuthorizationService _authorizationService; /// public Import( - IWorkflowDefinitionService workflowDefinitionService, IWorkflowDefinitionImporter workflowDefinitionImporter, - WorkflowDefinitionMapper workflowDefinitionMapper, - IApiSerializer apiSerializer) + IWorkflowDefinitionLinker linker, + IAuthorizationService authorizationService) { - _workflowDefinitionService = workflowDefinitionService; _workflowDefinitionImporter = workflowDefinitionImporter; - _workflowDefinitionMapper = workflowDefinitionMapper; - _apiSerializer = apiSerializer; + _linker = linker; + _authorizationService = authorizationService; } /// @@ -46,7 +44,16 @@ internal class Import : ElsaEndpoint var isNew = string.IsNullOrWhiteSpace(definitionId); var result = await ImportSingleWorkflowDefinitionAsync(model, cancellationToken); var definition = result.WorkflowDefinition; - var updatedModel = await _workflowDefinitionMapper.MapAsync(definition, cancellationToken); + + var authorizationResult = _authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); + + if (!authorizationResult.Result.Succeeded) + { + await SendForbiddenAsync(cancellationToken); + return; + } + + var updatedModel = await _linker.MapAsync(definition, cancellationToken); if (result.Succeeded) { diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs index a87e9ee04..7d0a32a67 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs @@ -1,10 +1,13 @@ using System.IO.Compression; using Elsa.Abstractions; +using Elsa.Workflows.Api.Constants; +using Elsa.Workflows.Api.Requirements; using Elsa.Workflows.Contracts; using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Management.Mappers; using Elsa.Workflows.Management.Models; using JetBrains.Annotations; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.ImportFiles; @@ -19,18 +22,21 @@ internal class ImportFiles : ElsaEndpoint private readonly IWorkflowDefinitionImporter _workflowDefinitionImporter; private readonly WorkflowDefinitionMapper _workflowDefinitionMapper; private readonly IApiSerializer _apiSerializer; + private readonly IAuthorizationService _authorizationService; /// public ImportFiles( IWorkflowDefinitionService workflowDefinitionService, IWorkflowDefinitionImporter workflowDefinitionImporter, WorkflowDefinitionMapper workflowDefinitionMapper, - IApiSerializer apiSerializer) + IApiSerializer apiSerializer, + IAuthorizationService authorizationService) { _workflowDefinitionService = workflowDefinitionService; _workflowDefinitionImporter = workflowDefinitionImporter; _workflowDefinitionMapper = workflowDefinitionMapper; _apiSerializer = apiSerializer; + _authorizationService = authorizationService; } /// @@ -44,14 +50,22 @@ internal class ImportFiles : ElsaEndpoint /// public override async Task HandleAsync(WorkflowDefinitionModel model, CancellationToken cancellationToken) { + var authorizationResult = _authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy); + + if (!authorizationResult.Result.Succeeded) + { + await SendForbiddenAsync(cancellationToken); + return; + } + if (Files.Any()) { var count = await ImportFilesAsync(Files, cancellationToken); - + if (!ValidationFailed) await SendOkAsync(new { Count = count }, cancellationToken); } - + if (ValidationFailed) await SendErrorsAsync(400, cancellationToken); } @@ -59,7 +73,7 @@ internal class ImportFiles : ElsaEndpoint private async Task ImportFilesAsync(IFormFileCollection files, CancellationToken cancellationToken) { var count = 0; - + foreach (var file in files) { var fileStream = file.OpenReadStream(); @@ -89,7 +103,7 @@ internal class ImportFiles : ElsaEndpoint } } } - + return count; } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/IsNameUnique/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/IsNameUnique/Endpoint.cs index 1b41b5a3d..d0fc95256 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/IsNameUnique/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/IsNameUnique/Endpoint.cs @@ -8,15 +8,8 @@ namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.IsNameUnique; /// Checks if a workflow definition name is unique. /// [PublicAPI] -internal class IsNameUnique : ElsaEndpoint +internal class IsNameUnique(IWorkflowDefinitionStore store) : ElsaEndpoint { - private readonly IWorkflowDefinitionStore _store; - - public IsNameUnique(IWorkflowDefinitionStore store) - { - _store = store; - } - public override void Configure() { Get("/workflow-definitions/validation/is-name-unique"); @@ -25,7 +18,7 @@ internal class IsNameUnique : ElsaEndpoint public override async Task HandleAsync(Request request, CancellationToken cancellationToken) { - var isUnique = await _store.GetIsNameUnique(request.Name.Trim(), request.DefinitionId, cancellationToken); + var isUnique = await store.GetIsNameUnique(request.Name.Trim(), request.DefinitionId, cancellationToken); var response = new Response(isUnique); await SendOkAsync(response, cancellationToken); diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/List/Endpoint.cs index 80aea41ff..ddbbf5c63 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/List/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/List/Endpoint.cs @@ -11,7 +11,7 @@ using JetBrains.Annotations; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.List; [PublicAPI] -internal class List(IWorkflowDefinitionStore store) : ElsaEndpoint> +internal class List(IWorkflowDefinitionStore store, IWorkflowDefinitionLinker linker) : ElsaEndpoint> { public override void Configure() { @@ -19,12 +19,14 @@ internal class List(IWorkflowDefinitionStore store) : ElsaEndpoint> ExecuteAsync(Request request, CancellationToken cancellationToken) + public override async Task> ExecuteAsync(Request request, CancellationToken cancellationToken) { var pageArgs = PageArgs.FromPage(request.Page, request.PageSize); var filter = CreateFilter(request); var summaries = await FindAsync(request, filter, pageArgs, cancellationToken); - return new PagedListResponse(summaries); + var pagedList = new PagedListResponse(summaries); + var response = linker.MapAsync(pagedList); + return response; } private WorkflowDefinitionFilter CreateFilter(Request request) diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs index 23ce77e01..5c26bba3c 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs @@ -1,45 +1,35 @@ +using System.Text.Json; using Elsa.Abstractions; using Elsa.Common.Models; +using Elsa.Extensions; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Api.Constants; +using Elsa.Workflows.Api.Models; +using Elsa.Workflows.Api.Requirements; +using Elsa.Workflows.Contracts; +using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Management.Mappers; using Elsa.Workflows.Management.Materializers; using Elsa.Workflows.Management.Models; -using JetBrains.Annotations; -using Medallion.Threading; -using System.Text.Json; -using Elsa.Extensions; -using Elsa.Workflows.Activities; -using Elsa.Workflows.Contracts; -using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Models; using Elsa.Workflows.Serialization.Converters; +using JetBrains.Annotations; +using Medallion.Threading; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; -using Elsa.Extensions; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Post; [PublicAPI] -internal class Post : ElsaEndpoint +internal class Post( + IApiSerializer serializer, + IWorkflowDefinitionPublisher workflowDefinitionPublisher, + VariableDefinitionMapper variableDefinitionMapper, + IDistributedLockProvider distributedLockProvider, + IWorkflowDefinitionLinker linker, + IAuthorizationService authorizationService) + : ElsaEndpoint { - private readonly IApiSerializer _serializer; - private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher; - private readonly VariableDefinitionMapper _variableDefinitionMapper; - private readonly WorkflowDefinitionMapper _workflowDefinitionMapper; - private readonly IDistributedLockProvider _distributedLockProvider; - - public Post( - IApiSerializer serializer, - IWorkflowDefinitionPublisher workflowDefinitionPublisher, - VariableDefinitionMapper variableDefinitionMapper, - WorkflowDefinitionMapper workflowDefinitionMapper, - IDistributedLockProvider distributedLockProvider) - { - _serializer = serializer; - _workflowDefinitionPublisher = workflowDefinitionPublisher; - _variableDefinitionMapper = variableDefinitionMapper; - _workflowDefinitionMapper = workflowDefinitionMapper; - _distributedLockProvider = distributedLockProvider; - } - public override void Configure() { Post("/workflow-definitions"); @@ -52,10 +42,10 @@ internal class Post : ElsaEndpoint(); var outputs = model.Outputs ?? new List(); var outcomes = model.Outcomes ?? new List(); @@ -96,11 +94,11 @@ internal class Post : ElsaEndpoint(new { definitionId }, response, cancellation: cancellationToken); diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Endpoint.cs index a403c775e..87b0bb0d3 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Endpoint.cs @@ -1,33 +1,23 @@ using Elsa.Abstractions; using Elsa.Common.Models; using Elsa.Extensions; +using Elsa.Workflows.Api.Constants; +using Elsa.Workflows.Api.Models; +using Elsa.Workflows.Api.Requirements; using Elsa.Workflows.Contracts; using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Management.Filters; -using Elsa.Workflows.Management.Mappers; -using Elsa.Workflows.Management.Models; using Elsa.Workflows.Serialization.Converters; using JetBrains.Annotations; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Publish; [PublicAPI] -internal class Publish : ElsaEndpoint +internal class Publish(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher, IApiSerializer serializer, IWorkflowDefinitionLinker linker, IAuthorizationService authorizationService) + : ElsaEndpoint { - private readonly IWorkflowDefinitionStore _store; - private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher; - private readonly IApiSerializer _serializer; - private readonly WorkflowDefinitionMapper _workflowDefinitionMapper; - - public Publish(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher, IApiSerializer serializer, WorkflowDefinitionMapper workflowDefinitionMapper) - { - _store = store; - _workflowDefinitionPublisher = workflowDefinitionPublisher; - _serializer = serializer; - _workflowDefinitionMapper = workflowDefinitionMapper; - } - public override void Configure() { Post("/workflow-definitions/{definitionId}/publish"); @@ -42,7 +32,7 @@ internal class Publish : ElsaEndpoint VersionOptions = VersionOptions.Latest }; - var definition = await _store.FindAsync(filter, cancellationToken); + var definition = await store.FindAsync(filter, cancellationToken); if (definition == null) { @@ -50,6 +40,14 @@ internal class Publish : ElsaEndpoint return; } + var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); + + if (!authorizationResult.Result.Succeeded) + { + await SendForbiddenAsync(cancellationToken); + return; + } + if (definition.IsPublished) { AddError($"Workflow with id {request.DefinitionId} is already published"); @@ -57,12 +55,12 @@ internal class Publish : ElsaEndpoint return; } - await _workflowDefinitionPublisher.PublishAsync(definition, cancellationToken); + await workflowDefinitionPublisher.PublishAsync(definition, cancellationToken); - var response = await _workflowDefinitionMapper.MapAsync(definition, cancellationToken); + var response = await linker.MapAsync(definition, cancellationToken); // We do not want to include composite root activities in the response. - var serializerOptions = _serializer.GetOptions().Clone(); + var serializerOptions = serializer.GetOptions().Clone(); serializerOptions.Converters.Add(new JsonIgnoreCompositeRootConverterFactory()); await HttpContext.Response.WriteAsJsonAsync(response, serializerOptions, cancellationToken); diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Retract/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Retract/Endpoint.cs index ec66bd5af..98ac80c04 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Retract/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Retract/Endpoint.cs @@ -1,27 +1,19 @@ using Elsa.Abstractions; using Elsa.Common.Models; +using Elsa.Workflows.Api.Constants; +using Elsa.Workflows.Api.Models; +using Elsa.Workflows.Api.Requirements; using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Management.Filters; -using Elsa.Workflows.Management.Mappers; -using Elsa.Workflows.Management.Models; using JetBrains.Annotations; +using Microsoft.AspNetCore.Authorization; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Retract; [PublicAPI] -internal class Retract : ElsaEndpoint +internal class Retract(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher, IWorkflowDefinitionLinker linker, IAuthorizationService authorizationService) + : ElsaEndpoint { - private readonly IWorkflowDefinitionStore _store; - private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher; - private readonly WorkflowDefinitionMapper _workflowDefinitionMapper; - - public Retract(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher, WorkflowDefinitionMapper workflowDefinitionMapper) - { - _store = store; - _workflowDefinitionPublisher = workflowDefinitionPublisher; - _workflowDefinitionMapper = workflowDefinitionMapper; - } - public override void Configure() { Post("/workflow-definitions/{definitionId}/retract"); @@ -35,8 +27,8 @@ internal class Retract : ElsaEndpoint DefinitionId = request.DefinitionId, VersionOptions = VersionOptions.LatestOrPublished }; - - var definition = await _store.FindAsync(filter, cancellationToken); + + var definition = await store.FindAsync(filter, cancellationToken); if (definition == null) { @@ -44,6 +36,14 @@ internal class Retract : ElsaEndpoint return; } + var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); + + if (!authorizationResult.Result.Succeeded) + { + await SendForbiddenAsync(cancellationToken); + return; + } + if (!definition.IsPublished) { AddError($"Workflow with id {request.DefinitionId} is not published"); @@ -51,8 +51,8 @@ internal class Retract : ElsaEndpoint return; } - await _workflowDefinitionPublisher.RetractAsync(definition, cancellationToken); - var response = await _workflowDefinitionMapper.MapAsync(definition, cancellationToken); + await workflowDefinitionPublisher.RetractAsync(definition, cancellationToken); + var response = await linker.MapAsync(definition, cancellationToken); await SendOkAsync(response, cancellationToken); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/UpdateReferences/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/UpdateReferences/Endpoint.cs index 7f3fd4975..ad26e3639 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/UpdateReferences/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/UpdateReferences/Endpoint.cs @@ -1,23 +1,18 @@ using Elsa.Abstractions; using Elsa.Common.Models; +using Elsa.Workflows.Api.Constants; +using Elsa.Workflows.Api.Requirements; using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Management.Filters; using JetBrains.Annotations; +using Microsoft.AspNetCore.Authorization; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.UpdateReferences; [PublicAPI] -internal class UpdateReferences : ElsaEndpoint +internal class UpdateReferences(IWorkflowDefinitionStore store, IWorkflowDefinitionManager workflowDefinitionManager, IAuthorizationService authorizationService) + : ElsaEndpoint { - private readonly IWorkflowDefinitionStore _store; - private readonly IWorkflowDefinitionManager _workflowDefinitionManager; - - public UpdateReferences(IWorkflowDefinitionStore store, IWorkflowDefinitionManager workflowDefinitionManager) - { - _store = store; - _workflowDefinitionManager = workflowDefinitionManager; - } - public override void Configure() { Post("/workflow-definitions/{definitionId}/update-references"); @@ -32,7 +27,7 @@ internal class UpdateReferences : ElsaEndpoint VersionOptions = VersionOptions.Latest }; - var definition = await _store.FindAsync(filter, cancellationToken); + var definition = await store.FindAsync(filter, cancellationToken); if (definition == null) { @@ -40,7 +35,15 @@ internal class UpdateReferences : ElsaEndpoint return; } - var affectedWorkflows = await _workflowDefinitionManager.UpdateReferencesInConsumingWorkflows(definition, cancellationToken); + var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); + + if (!authorizationResult.Result.Succeeded) + { + await SendForbiddenAsync(cancellationToken); + return; + } + + var affectedWorkflows = await workflowDefinitionManager.UpdateReferencesInConsumingWorkflows(definition, cancellationToken); var response = new Response(affectedWorkflows.Select(w => w.Name ?? w.DefinitionId)); await SendOkAsync(response, cancellationToken); } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/Delete.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/Delete.cs index 905522328..aea5c8a69 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/Delete.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/Delete.cs @@ -1,7 +1,11 @@ using Elsa.Abstractions; +using Elsa.Common.Models; +using Elsa.Workflows.Api.Constants; +using Elsa.Workflows.Api.Requirements; using Elsa.Workflows.Management.Contracts; -using FastEndpoints; +using Elsa.Workflows.Management.Filters; using JetBrains.Annotations; +using Microsoft.AspNetCore.Authorization; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Version; @@ -12,11 +16,15 @@ namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Version; public class DeleteVersion : ElsaEndpointWithoutRequest { private readonly IWorkflowDefinitionManager _workflowDefinitionManager; + private readonly IAuthorizationService _authorizationService; + private readonly IWorkflowDefinitionStore _store; /// - public DeleteVersion(IWorkflowDefinitionManager workflowDefinitionManager) + public DeleteVersion(IWorkflowDefinitionManager workflowDefinitionManager, IAuthorizationService authorizationService, IWorkflowDefinitionStore store) { _workflowDefinitionManager = workflowDefinitionManager; + _authorizationService = authorizationService; + _store = store; } /// @@ -27,19 +35,41 @@ public class DeleteVersion : ElsaEndpointWithoutRequest } /// - public override async Task HandleAsync(CancellationToken ct) + public override async Task HandleAsync(CancellationToken cancellationToken) { var definitionId = Route("definitionId")!; var version = Route("version"); - - var result = await _workflowDefinitionManager.DeleteVersionAsync(definitionId, version, ct); - - if (!result) + + var filter = new WorkflowDefinitionFilter { - await SendNotFoundAsync(ct); + DefinitionId = definitionId, + VersionOptions = VersionOptions.SpecificVersion(version) + }; + + var definition = await _store.FindAsync(filter, cancellationToken); + + if (definition == null) + { + await SendNotFoundAsync(cancellationToken); return; } - await SendOkAsync(ct); + var authorizationResult = _authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); + + if (!authorizationResult.Result.Succeeded) + { + await SendForbiddenAsync(cancellationToken); + return; + } + + var result = await _workflowDefinitionManager.DeleteVersionAsync(definition, cancellationToken); + + if (!result) + { + await SendNotFoundAsync(cancellationToken); + return; + } + + await SendOkAsync(cancellationToken); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/List.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/List.cs index 40e4b1903..42475983c 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/List.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/List.cs @@ -3,22 +3,14 @@ using Elsa.Common.Entities; using Elsa.Common.Models; using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Management.Filters; -using FastEndpoints; using JetBrains.Annotations; using Microsoft.AspNetCore.Http; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Version; [PublicAPI] -internal class ListVersions : ElsaEndpointWithoutRequest +internal class ListVersions(IWorkflowDefinitionStore store) : ElsaEndpointWithoutRequest { - private readonly IWorkflowDefinitionStore _store; - - public ListVersions(IWorkflowDefinitionStore store) - { - _store = store; - } - public override void Configure() { Get("workflow-definitions/{definitionId}/versions"); @@ -36,7 +28,7 @@ internal class ListVersions : ElsaEndpointWithoutRequest }; var orderBy = new WorkflowDefinitionOrder(x => x.Version, OrderDirection.Descending); - var definitions = await _store.FindManyAsync(filter, orderBy, cancellationToken); + var definitions = await store.FindManyAsync(filter, orderBy, cancellationToken); if (!definitions.Any()) { diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/Revert.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/Revert.cs index 4a2c2572e..707efee28 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/Revert.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/Revert.cs @@ -1,33 +1,53 @@ using Elsa.Abstractions; +using Elsa.Common.Models; +using Elsa.Workflows.Api.Constants; +using Elsa.Workflows.Api.Requirements; using Elsa.Workflows.Management.Contracts; -using FastEndpoints; +using Elsa.Workflows.Management.Filters; using JetBrains.Annotations; +using Microsoft.AspNetCore.Authorization; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Version; [PublicAPI] -internal class RevertVersion : ElsaEndpointWithoutRequest +internal class RevertVersion(IWorkflowDefinitionManager workflowDefinitionManager, IAuthorizationService authorizationService, IWorkflowDefinitionStore store) + : ElsaEndpointWithoutRequest { - private readonly IWorkflowDefinitionManager _workflowDefinitionManager; - - public RevertVersion(IWorkflowDefinitionManager workflowDefinitionManager) - { - _workflowDefinitionManager = workflowDefinitionManager; - } - public override void Configure() { Post("workflow-definitions/{definitionId}/revert/{version}"); ConfigurePermissions("publish:workflow-definitions"); } - public override async Task HandleAsync(CancellationToken ct) + public override async Task HandleAsync(CancellationToken cancellationToken) { var definitionId = Route("definitionId")!; var version = Route("version"); - await _workflowDefinitionManager.RevertVersionAsync(definitionId, version, ct); - - await SendOkAsync(ct); + var filter = new WorkflowDefinitionFilter + { + DefinitionId = definitionId, + VersionOptions = VersionOptions.SpecificVersion(version) + }; + + var definition = await store.FindAsync(filter, cancellationToken); + + if (definition == null) + { + await SendNotFoundAsync(cancellationToken); + return; + } + + var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); + + if (!authorizationResult.Result.Succeeded) + { + await SendForbiddenAsync(cancellationToken); + return; + } + + await workflowDefinitionManager.RevertVersionAsync(definitionId, version, cancellationToken); + + await SendOkAsync(cancellationToken); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/BulkCancel/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/BulkCancel/Models.cs index 0129f37c6..72d71144a 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/BulkCancel/Models.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/BulkCancel/Models.cs @@ -28,12 +28,7 @@ public class Request public VersionOptions? VersionOptions { get; set; } } -public class Response +public class Response(int cancelledCount) { - public Response(int cancelledCount) - { - CancelledCount = cancelledCount; - } - - [JsonPropertyName("cancelled")] public int CancelledCount { get; } + [JsonPropertyName("cancelled")] public int CancelledCount { get; } = cancelledCount; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/BulkDelete/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/BulkDelete/Endpoint.cs index 97363d3d6..67cc1dba1 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/BulkDelete/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/BulkDelete/Endpoint.cs @@ -6,15 +6,8 @@ using JetBrains.Annotations; namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.BulkDelete; [PublicAPI] -internal class BulkDelete : ElsaEndpoint +internal class BulkDelete(IWorkflowInstanceManager store) : ElsaEndpoint { - private readonly IWorkflowInstanceManager _store; - - public BulkDelete(IWorkflowInstanceManager store) - { - _store = store; - } - public override void Configure() { Post("/bulk-actions/delete/workflow-instances/by-id"); @@ -24,7 +17,7 @@ internal class BulkDelete : ElsaEndpoint public override async Task ExecuteAsync(Request request, CancellationToken cancellationToken) { var filter = new WorkflowInstanceFilter { Ids = request.Ids }; - var count = await _store.BulkDeleteAsync(filter, cancellationToken); + var count = await store.BulkDeleteAsync(filter, cancellationToken); return new Response(count); } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/BulkDelete/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/BulkDelete/Models.cs index 7857427ea..b58697cc1 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/BulkDelete/Models.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/BulkDelete/Models.cs @@ -7,12 +7,7 @@ internal class Request public ICollection Ids { get; set; } = default!; } -internal class Response +internal class Response(long deletedCount) { - public Response(long deletedCount) - { - DeletedCount = deletedCount; - } - - [JsonPropertyName("deleted")] public long DeletedCount { get; } + [JsonPropertyName("deleted")] public long DeletedCount { get; } = deletedCount; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Delete/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Delete/Endpoint.cs index c23b8275d..526bdeba5 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Delete/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Delete/Endpoint.cs @@ -6,11 +6,8 @@ using JetBrains.Annotations; namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.Delete; [PublicAPI] -internal class Delete : ElsaEndpoint +internal class Delete(IWorkflowInstanceManager store) : ElsaEndpoint { - private readonly IWorkflowInstanceManager _store; - public Delete(IWorkflowInstanceManager store) => _store = store; - public override void Configure() { Delete("/workflow-instances/{id}"); @@ -20,7 +17,7 @@ internal class Delete : ElsaEndpoint public override async Task HandleAsync(Request request, CancellationToken cancellationToken) { var filter = new WorkflowInstanceFilter { Id = request.Id }; - var deleted = await _store.DeleteAsync(filter, cancellationToken); + var deleted = await store.DeleteAsync(filter, cancellationToken); if (deleted) await SendNoContentAsync(cancellationToken); diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Get/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Get/Endpoint.cs index 46617af9f..1df4ed7a9 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Get/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Get/Endpoint.cs @@ -7,15 +7,8 @@ using JetBrains.Annotations; namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.Get; [PublicAPI] -internal class Get : ElsaEndpoint +internal class Get(IWorkflowInstanceStore store) : ElsaEndpoint { - private readonly IWorkflowInstanceStore _store; - - public Get(IWorkflowInstanceStore store) - { - _store = store; - } - public override void Configure() { Get("/workflow-instances/{id}"); @@ -25,7 +18,7 @@ internal class Get : ElsaEndpoint? EventNames { get; set; } } -internal class Response +internal class Response(ICollection items, long totalCount) { - public Response(ICollection items, long totalCount) - { - Items = items; - TotalCount = totalCount; - } - - public ICollection Items { get; } - public long TotalCount { get; } + public ICollection Items { get; } = items; + public long TotalCount { get; } = totalCount; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Journal/List/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Journal/List/Models.cs index b19ac1c05..7d73324c1 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Journal/List/Models.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Journal/List/Models.cs @@ -36,14 +36,8 @@ internal class Request public int? Take { get; set; } } -internal class Response +internal class Response(ICollection items, long totalCount) { - public Response(ICollection items, long totalCount) - { - Items = items; - TotalCount = totalCount; - } - - public ICollection Items { get; } - public long TotalCount { get; } + public ICollection Items { get; } = items; + public long TotalCount { get; } = totalCount; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Features/WorkflowsApiFeature.cs b/src/modules/Elsa.Workflows.Api/Features/WorkflowsApiFeature.cs index 9f1a3240d..6fb8a3003 100644 --- a/src/modules/Elsa.Workflows.Api/Features/WorkflowsApiFeature.cs +++ b/src/modules/Elsa.Workflows.Api/Features/WorkflowsApiFeature.cs @@ -4,9 +4,14 @@ using Elsa.Features.Attributes; using Elsa.Features.Services; using Elsa.Http.Features; using Elsa.SasTokens.Features; +using Elsa.Workflows.Api.Constants; +using Elsa.Workflows.Api.Requirements; using Elsa.Workflows.Api.Serialization; +using Elsa.Workflows.Api.Services; using Elsa.Workflows.Management.Features; using Elsa.Workflows.Runtime.Features; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Api.Features; @@ -36,5 +41,13 @@ public class WorkflowsApiFeature : FeatureBase { Services.AddSerializationOptionsConfigurator(); Module.AddFastEndpointsFromModule(); + + Services.AddScoped(); + + Services.AddScoped(); + Services.Configure(options => + { + options.AddPolicy(AuthorizationPolicies.NotReadOnlyPolicy, policy => policy.AddRequirements(new NotReadOnlyRequirement())); + }); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Middleware/JsonSerializationErrorHandlerMiddleware.cs b/src/modules/Elsa.Workflows.Api/Middleware/JsonSerializationErrorHandlerMiddleware.cs index 5b254891c..08482dcb2 100644 --- a/src/modules/Elsa.Workflows.Api/Middleware/JsonSerializationErrorHandlerMiddleware.cs +++ b/src/modules/Elsa.Workflows.Api/Middleware/JsonSerializationErrorHandlerMiddleware.cs @@ -6,11 +6,8 @@ namespace Elsa.Workflows.Api.Middleware; /// /// Catches JSON serialization errors during POST requests and returns Bad Request responses. /// -public class JsonSerializationErrorHandlerMiddleware +public class JsonSerializationErrorHandlerMiddleware(RequestDelegate next) { - private readonly RequestDelegate _next; - public JsonSerializationErrorHandlerMiddleware(RequestDelegate next) => _next = next; - public async Task InvokeAsync(HttpContext httpContext) { var method = httpContext.Request.Method; @@ -18,13 +15,13 @@ public class JsonSerializationErrorHandlerMiddleware // If the HTTP verb is anything but POST or PUT, do nothing. if (!string.Equals(method, "POST", StringComparison.InvariantCultureIgnoreCase) && !string.Equals(method, "PUT", StringComparison.InvariantCultureIgnoreCase)) { - await _next(httpContext); + await next(httpContext); return; } try { - await _next(httpContext); + await next(httpContext); } catch (JsonException e) { diff --git a/src/modules/Elsa.Workflows.Api/Models/LinkedWorkflowDefinitionModel.cs b/src/modules/Elsa.Workflows.Api/Models/LinkedWorkflowDefinitionModel.cs new file mode 100644 index 000000000..ae4aa8233 --- /dev/null +++ b/src/modules/Elsa.Workflows.Api/Models/LinkedWorkflowDefinitionModel.cs @@ -0,0 +1,6 @@ +using Elsa.Models; +using Elsa.Workflows.Management.Models; + +namespace Elsa.Workflows.Api.Models; + +public record LinkedWorkflowDefinitionModel(Link[]? Links) : WorkflowDefinitionModel; diff --git a/src/modules/Elsa.Workflows.Api/Models/LinkedWorkflowDefinitionSummary.cs b/src/modules/Elsa.Workflows.Api/Models/LinkedWorkflowDefinitionSummary.cs new file mode 100644 index 000000000..ebf28f35f --- /dev/null +++ b/src/modules/Elsa.Workflows.Api/Models/LinkedWorkflowDefinitionSummary.cs @@ -0,0 +1,9 @@ +using Elsa.Models; +using Elsa.Workflows.Management.Models; + +namespace Elsa.Workflows.Api.Models; + +public class LinkedWorkflowDefinitionSummary : WorkflowDefinitionSummary +{ + public Link[]? Links { get; set; } +} diff --git a/src/modules/Elsa.Workflows.Api/Requirements/NotReadOnlyRequirement.cs b/src/modules/Elsa.Workflows.Api/Requirements/NotReadOnlyRequirement.cs new file mode 100644 index 000000000..594896485 --- /dev/null +++ b/src/modules/Elsa.Workflows.Api/Requirements/NotReadOnlyRequirement.cs @@ -0,0 +1,43 @@ +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Management.Options; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; + +namespace Elsa.Workflows.Api.Requirements; + +public record NotReadOnlyResource(WorkflowDefinition? WorkflowDefinition = default); + + +public record NotReadOnlyRequirement() : IAuthorizationRequirement; + + +/// +[PublicAPI] +public class NotReadOnlyRequirementHandler : AuthorizationHandler +{ + private readonly IOptions _managementOptions; + + /// + public NotReadOnlyRequirementHandler( + IOptions managementOptions) + { + _managementOptions = managementOptions; + } + + /// + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, NotReadOnlyRequirement requirement, NotReadOnlyResource resource) + { + if (_managementOptions.Value.IsReadOnlyMode) + { + context.Fail(new AuthorizationFailureReason(this, "Workflow edit is not allowed when the read-only mode is enabled.")); + } + + if (resource.WorkflowDefinition != null && (resource.WorkflowDefinition.IsReadonly || resource.WorkflowDefinition.IsSystem)) + { + context.Fail(new AuthorizationFailureReason(this, "Workflow edit is not allowed for a readonly or system workflow.")); + } + + context.Succeed(requirement); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverterFactory.cs b/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverterFactory.cs index 633f233e9..05545fa1f 100644 --- a/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverterFactory.cs +++ b/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverterFactory.cs @@ -5,19 +5,12 @@ using Elsa.Workflows.Models; namespace Elsa.Workflows.Api.Serialization; -internal class ArgumentJsonConverterFactory : JsonConverterFactory +internal class ArgumentJsonConverterFactory(IWellKnownTypeRegistry wellKnownTypeRegistry) : JsonConverterFactory { - private readonly IWellKnownTypeRegistry _wellKnownTypeRegistry; - - public ArgumentJsonConverterFactory(IWellKnownTypeRegistry wellKnownTypeRegistry) - { - _wellKnownTypeRegistry = wellKnownTypeRegistry; - } - public override bool CanConvert(Type typeToConvert) => typeToConvert.IsAssignableTo(typeof(ArgumentDefinition)); public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new ArgumentJsonConverter(_wellKnownTypeRegistry); + return new ArgumentJsonConverter(wellKnownTypeRegistry); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Serialization/SerializationConfigurator.cs b/src/modules/Elsa.Workflows.Api/Serialization/SerializationConfigurator.cs index 3c8b19c80..3b1b5cca8 100644 --- a/src/modules/Elsa.Workflows.Api/Serialization/SerializationConfigurator.cs +++ b/src/modules/Elsa.Workflows.Api/Serialization/SerializationConfigurator.cs @@ -3,17 +3,10 @@ using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Api.Serialization; -internal class SerializationConfigurator : SerializationOptionsConfiguratorBase +internal class SerializationConfigurator(IServiceProvider serviceProvider) : SerializationOptionsConfiguratorBase { - private readonly IServiceProvider _serviceProvider; - - public SerializationConfigurator(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - public override void Configure(JsonSerializerOptions options) { - options.Converters.Add(ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider)); + options.Converters.Add(ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider)); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Services/StaticWorkflowDefinitionLinker.cs b/src/modules/Elsa.Workflows.Api/Services/StaticWorkflowDefinitionLinker.cs new file mode 100644 index 000000000..da2a3351e --- /dev/null +++ b/src/modules/Elsa.Workflows.Api/Services/StaticWorkflowDefinitionLinker.cs @@ -0,0 +1,165 @@ +using Elsa.Models; +using Elsa.Workflows.Api.Models; +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Management.Mappers; +using Elsa.Workflows.Management.Models; +using Elsa.Workflows.Management.Options; +using Microsoft.Extensions.Options; + +namespace Elsa.Workflows.Api.Services; + +/// +public class StaticWorkflowDefinitionLinker( + IOptions managementOptions, + WorkflowDefinitionMapper workflowDefinitionMapper) + : IWorkflowDefinitionLinker +{ + /// + public async Task MapAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default) + { + var workflowDefinitionModel = await workflowDefinitionMapper.MapAsync(definition, cancellationToken); + var linkedModel = new LinkedWorkflowDefinitionModel(GenerateLinksForSingleEntry(definition.DefinitionId, definition.IsReadonly)) + { + Id = workflowDefinitionModel.Id, + DefinitionId = workflowDefinitionModel.DefinitionId, + Name = workflowDefinitionModel.Name, + Description = workflowDefinitionModel.Description, + CreatedAt = workflowDefinitionModel.CreatedAt, + Version = workflowDefinitionModel.Version, + ToolVersion = workflowDefinitionModel.ToolVersion, + Variables = workflowDefinitionModel.Variables, + Inputs = workflowDefinitionModel.Inputs, + Outputs = workflowDefinitionModel.Outputs, + Outcomes = workflowDefinitionModel.Outcomes, + CustomProperties = workflowDefinitionModel.CustomProperties, + IsReadonly = workflowDefinitionModel.IsReadonly, + IsSystem = workflowDefinitionModel.IsSystem, + IsLatest = workflowDefinitionModel.IsLatest, + IsPublished = workflowDefinitionModel.IsPublished, + Options = workflowDefinitionModel.Options, + UsableAsActivity = workflowDefinitionModel.UsableAsActivity, + Root = workflowDefinitionModel.Root + }; + + return linkedModel; + } + + /// + public PagedListResponse MapAsync(PagedListResponse list, CancellationToken cancellationToken = default) + { + var items = new List(); + + foreach (var item in list.Items) + { + items.Add(new LinkedWorkflowDefinitionSummary + { + Links = GenerateLinksForSingleEntry(item.DefinitionId, item.IsReadonly), + Id = item.Id, + DefinitionId = item.DefinitionId, + Name = item.Name, + Description = item.Description, + Version = item.Version, + ToolVersion = item.ToolVersion, + IsLatest = item.IsLatest, + IsPublished = item.IsPublished, + ProviderName = item.ProviderName, + MaterializerName = item.MaterializerName, + CreatedAt = item.CreatedAt, + IsReadonly = item.IsReadonly + }); + } + + return new PagedListResponse + { + TotalCount = list.TotalCount, + Items = items, + Links = GenerateLinksForPagedList().ToArray() + }; + } + + /// + public async Task> MapAsync(List definitions, CancellationToken cancellationToken = default) + { + var models = (await workflowDefinitionMapper.MapAsync(definitions, cancellationToken)).ToList(); + var result = new List(); + + foreach (var item in models) + { + var linkedModel = new LinkedWorkflowDefinitionModel(GenerateLinksForSingleEntry(item.DefinitionId, item.IsReadonly)) + { + Id = item.Id, + DefinitionId = item.DefinitionId, + Name = item.Name, + Description = item.Description, + CreatedAt = item.CreatedAt, + Version = item.Version, + ToolVersion = item.ToolVersion, + Variables = item.Variables, + Inputs = item.Inputs, + Outputs = item.Outputs, + Outcomes = item.Outcomes, + CustomProperties = item.CustomProperties, + IsReadonly = item.IsReadonly, + IsSystem = item.IsSystem, + IsLatest = item.IsLatest, + IsPublished = item.IsPublished, + Options = item.Options, + UsableAsActivity = item.UsableAsActivity, + Root = item.Root + }; + + result.Add(linkedModel); + } + + return result; + } + + private Link[] GenerateLinksForPagedList() + { + var linksList = new List + { + new($"/workflow-definitions", "self", "GET"), + new($"/workflow-definitions/validation/is-name-unique", "is-name-unique", "GET"), + new($"/workflow-definitions/query/count", "count", "GET"), + new($"/workflow-definitions/many-by-id", "many-by-id", "GET") + }; + + if (!managementOptions.Value.IsReadOnlyMode) + { + linksList.Add(new Link($"/bulk-actions/delete/workflow-definitions/by-definition-id", "bulk-delete-by-definition-id", "POST")); + linksList.Add(new Link($"/bulk-actions/delete/workflow-definitions/by-id", "bulk-delete-by-id", "POST")); + linksList.Add(new Link($"/bulk-actions/publish/workflow-definitions/by-definition-ids", "bulk-publish", "POST")); + linksList.Add(new Link($"/bulk-actions/retract/workflow-definitions/by-definition-ids", "bulk-retract", "POST")); + linksList.Add(new Link($"/workflow-definitions/import", "import", "POST")); + linksList.Add(new Link($"/workflow-definitions/import-files", "import-files", "POST")); + linksList.Add(new Link($"/workflow-definitions", "create", "POST")); + } + + return linksList.ToArray(); + } + + private Link[] GenerateLinksForSingleEntry(string definitionId, bool definitionIsReadonly) + { + var links = new List + { + new($"/workflow-definitions/{definitionId}", "self", "GET"), + new($"/workflow-definitions/by-definition-id/{definitionId}", "self", "GET"), + new($"/workflow-definitions/{definitionId}/versions", "versions", "GET"), + new($"/workflow-definitions/{definitionId}/bulk-dispatch", "bulk-dispatch", "POST"), + new($"/workflow-definitions/{definitionId}/dispatch", "dispatch", "POST"), + new($"/workflow-definitions/{definitionId}/execute", "execute", "POST"), + new($"/workflow-definitions/{definitionId}/export", "export", "POST") + }; + + if (!managementOptions.Value.IsReadOnlyMode && !definitionIsReadonly) + { + links.Add(new Link($"/workflow-definitions/{definitionId}/publish", "publish", "POST")); + links.Add(new Link($"/workflow-definitions/{definitionId}/retract", "retract", "POST")); + links.Add(new Link($"/workflow-definitions/{definitionId}", "delete", "DELETE")); + links.Add(new Link($"/workflow-definitions/{definitionId}/import", "import", "PUT")); + links.Add(new Link($"/workflow-definitions/{definitionId}/update-references", "update-references", "POST")); + } + + return links.ToArray(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowDefinitionManager.cs b/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowDefinitionManager.cs index 670014873..7fddbc39d 100644 --- a/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowDefinitionManager.cs +++ b/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowDefinitionManager.cs @@ -31,7 +31,15 @@ public interface IWorkflowDefinitionManager /// The cancellation token. /// True if the workflow definition version was deleted, otherwise false. Task DeleteVersionAsync(string definitionId, int version, CancellationToken cancellationToken = default); - + + /// + /// Deletes a specific workflow definition version. + /// + /// + /// + /// + Task DeleteVersionAsync(WorkflowDefinition definitionToDelete, CancellationToken cancellationToken); + /// /// Deletes all workflow definition versions with the specified definition IDs. /// diff --git a/src/modules/Elsa.Workflows.Management/Elsa.Workflows.Management.csproj b/src/modules/Elsa.Workflows.Management/Elsa.Workflows.Management.csproj index 99b09454a..c2f98f223 100644 --- a/src/modules/Elsa.Workflows.Management/Elsa.Workflows.Management.csproj +++ b/src/modules/Elsa.Workflows.Management/Elsa.Workflows.Management.csproj @@ -19,6 +19,7 @@ + diff --git a/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs b/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs index 557b9551b..80ad1cc3b 100644 --- a/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs +++ b/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs @@ -47,6 +47,8 @@ public class WorkflowManagementFeature : FeatureBase private string CompressionAlgorithm { get; set; } = nameof(None); private LogPersistenceMode LogPersistenceMode { get; set; } = LogPersistenceMode.Include; + private bool IsReadOnlyMode { get; set; } + /// public WorkflowManagementFeature(IModule module) : base(module) { @@ -175,6 +177,16 @@ public class WorkflowManagementFeature : FeatureBase return this; } + /// + /// Enables or disables read-only mode for resources such as workflow definitions. + /// + /// + public WorkflowManagementFeature UseReadOnlyMode(bool enabled) + { + IsReadOnlyMode = enabled; + return this; + } + /// [RequiresUnreferencedCode("The assembly containing the specified marker type will be scanned for activity types.")] public override void Configure() @@ -229,6 +241,7 @@ public class WorkflowManagementFeature : FeatureBase options.CompressionAlgorithm = CompressionAlgorithm; options.LogPersistenceMode = LogPersistenceMode; + options.IsReadOnlyMode = IsReadOnlyMode; }); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Filters/WorkflowDefinitionFilter.cs b/src/modules/Elsa.Workflows.Management/Filters/WorkflowDefinitionFilter.cs index 968e2b824..eb6d89767 100644 --- a/src/modules/Elsa.Workflows.Management/Filters/WorkflowDefinitionFilter.cs +++ b/src/modules/Elsa.Workflows.Management/Filters/WorkflowDefinitionFilter.cs @@ -66,6 +66,11 @@ public class WorkflowDefinitionFilter /// public bool? IsSystem { get; set; } + /// + /// Filter on workflows that are read-only. + /// + public bool? IsReadonly { get; set; } + /// /// Applies the filter to the specified queryable. /// @@ -85,6 +90,7 @@ public class WorkflowDefinitionFilter if (filter.UsableAsActivity != null) queryable = queryable.Where(x => x.Options.UsableAsActivity == filter.UsableAsActivity); if (!string.IsNullOrWhiteSpace(filter.SearchTerm)) queryable = queryable.Where(x => x.Name!.Contains(filter.SearchTerm) || x.Description!.Contains(filter.SearchTerm) || x.Id.Contains(filter.SearchTerm) || x.DefinitionId.Contains(filter.SearchTerm)); if (filter.IsSystem != null) queryable = queryable.Where(x => x.IsSystem == filter.IsSystem); + if (filter.IsReadonly != null) queryable = queryable.Where(x => x.IsReadonly == filter.IsReadonly); return queryable; } diff --git a/src/modules/Elsa.Workflows.Management/Mappers/WorkflowDefinitionMapper.cs b/src/modules/Elsa.Workflows.Management/Mappers/WorkflowDefinitionMapper.cs index 846e6b3d8..a090db733 100644 --- a/src/modules/Elsa.Workflows.Management/Mappers/WorkflowDefinitionMapper.cs +++ b/src/modules/Elsa.Workflows.Management/Mappers/WorkflowDefinitionMapper.cs @@ -62,7 +62,7 @@ public class WorkflowDefinitionMapper var options = source.Options ?? new WorkflowOptions(); // TODO: Remove this in the future when users have migrated workflows to use the new UsableAsActivity options property. - + #pragma warning disable CS0618 options.UsableAsActivity ??= source.UsableAsActivity ?? false; #pragma warning restore CS0618 @@ -88,7 +88,7 @@ public class WorkflowDefinitionMapper /// The source s. /// An optional cancellation token. /// The mapped s. - public async Task> MapAsync(IEnumerable source, CancellationToken cancellationToken = default) => + public async Task> MapAsync(IEnumerable source, CancellationToken cancellationToken = default) => await Task.WhenAll(source.Select(async x => await MapAsync(x, cancellationToken))); /// @@ -124,7 +124,7 @@ public class WorkflowDefinitionMapper null, workflow.Root); } - + /// /// Maps a to a . /// diff --git a/src/modules/Elsa.Workflows.Management/Models/WorkflowDefinitionSummary.cs b/src/modules/Elsa.Workflows.Management/Models/WorkflowDefinitionSummary.cs index 6a46eb20a..a93331e32 100644 --- a/src/modules/Elsa.Workflows.Management/Models/WorkflowDefinitionSummary.cs +++ b/src/modules/Elsa.Workflows.Management/Models/WorkflowDefinitionSummary.cs @@ -1,4 +1,5 @@ using System.Linq.Expressions; +using Elsa.Models; using Elsa.Workflows.Management.Entities; using JetBrains.Annotations; @@ -25,7 +26,8 @@ public class WorkflowDefinitionSummary IsPublished = workflowDefinition.IsPublished, ProviderName = workflowDefinition.ProviderName, MaterializerName = workflowDefinition.MaterializerName, - CreatedAt = workflowDefinition.CreatedAt + CreatedAt = workflowDefinition.CreatedAt, + IsReadonly = workflowDefinition.IsReadonly }; /// @@ -43,7 +45,8 @@ public class WorkflowDefinitionSummary IsPublished = workflowDefinition.IsPublished, ProviderName = workflowDefinition.ProviderName, MaterializerName = workflowDefinition.MaterializerName, - CreatedAt = workflowDefinition.CreatedAt + CreatedAt = workflowDefinition.CreatedAt, + IsReadonly = workflowDefinition.IsReadonly }; /// diff --git a/src/modules/Elsa.Workflows.Management/Options/ManagementOptions.cs b/src/modules/Elsa.Workflows.Management/Options/ManagementOptions.cs index 82b70d550..e6f3c02ca 100644 --- a/src/modules/Elsa.Workflows.Management/Options/ManagementOptions.cs +++ b/src/modules/Elsa.Workflows.Management/Options/ManagementOptions.cs @@ -11,7 +11,7 @@ public class ManagementOptions /// A collection of activity types that are available to the system. /// public HashSet ActivityTypes { get; set; } = new(); - + /// /// A collection of types that are available to the system as variable types. /// @@ -26,4 +26,9 @@ public class ManagementOptions /// The default Log Persistence Mode to use for all the system /// public LogPersistenceMode LogPersistenceMode { get; set; } + + /// + /// A mode that does not allow editing workflows. + /// + public bool IsReadOnlyMode { get; set; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionManager.cs b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionManager.cs index fd3b7a934..d62374635 100644 --- a/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionManager.cs +++ b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionManager.cs @@ -67,7 +67,7 @@ public class WorkflowDefinitionManager : IWorkflowDefinitionManager { var definitionIdList = definitionIds.Distinct().ToList(); await _notificationSender.SendAsync(new WorkflowDefinitionsDeleting(definitionIdList), cancellationToken); - var filter = new WorkflowDefinitionFilter { DefinitionIds = definitionIdList }; + var filter = new WorkflowDefinitionFilter { DefinitionIds = definitionIdList, IsReadonly = false }; var count = await _store.DeleteAsync(filter, cancellationToken); await EnsureLastVersionIsLatestAsync(definitionIdList, cancellationToken); await _notificationSender.SendAsync(new WorkflowDefinitionsDeleted(definitionIdList), cancellationToken); @@ -100,6 +100,28 @@ public class WorkflowDefinitionManager : IWorkflowDefinitionManager return await DeleteVersionAsync(definitionToDelete, cancellationToken); } + /// + public async Task DeleteVersionAsync(WorkflowDefinition definitionToDelete, CancellationToken cancellationToken) + { + if (definitionToDelete.IsPublished) + { + throw new Exception("Published version cannot be deleted before retracting it"); + } + + await _notificationSender.SendAsync(new WorkflowDefinitionVersionDeleting(definitionToDelete), cancellationToken); + + var filter = new WorkflowDefinitionFilter { Id = definitionToDelete.Id }; + var isDeleted = await _store.DeleteAsync(filter, cancellationToken) > 0; + + if (!isDeleted) + return false; + + await EnsureLastVersionIsLatestAsync(definitionToDelete.DefinitionId, cancellationToken); + await _notificationSender.SendAsync(new WorkflowDefinitionVersionDeleted(definitionToDelete), cancellationToken); + + return isDeleted; + } + /// public async Task RevertVersionAsync(string definitionId, int version, CancellationToken cancellationToken = default) { @@ -130,7 +152,7 @@ public class WorkflowDefinitionManager : IWorkflowDefinitionManager { VersionOptions = VersionOptions.LatestOrPublished }, cancellationToken)).ToList(); - + // Remove the dependency from the list of workflow definitions to consider. workflowDefinitions = workflowDefinitions.Where(x => x.DefinitionId != dependency.DefinitionId).ToList(); @@ -168,27 +190,6 @@ public class WorkflowDefinitionManager : IWorkflowDefinitionManager return updatedWorkflowDefinitions; } - private async Task DeleteVersionAsync(WorkflowDefinition definitionToDelete, CancellationToken cancellationToken) - { - if (definitionToDelete.IsPublished) - { - throw new Exception("Published version cannot be deleted before retracting it"); - } - - await _notificationSender.SendAsync(new WorkflowDefinitionVersionDeleting(definitionToDelete), cancellationToken); - - var filter = new WorkflowDefinitionFilter { Id = definitionToDelete.Id }; - var isDeleted = await _store.DeleteAsync(filter, cancellationToken) > 0; - - if (!isDeleted) - return false; - - await EnsureLastVersionIsLatestAsync(definitionToDelete.DefinitionId, cancellationToken); - await _notificationSender.SendAsync(new WorkflowDefinitionVersionDeleted(definitionToDelete), cancellationToken); - - return isDeleted; - } - private async Task EnsureLastVersionIsLatestAsync(IEnumerable definitionIds, CancellationToken cancellationToken) { foreach (var definitionId in definitionIds) diff --git a/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs b/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs index 4f15b0b30..df2afc214 100644 --- a/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs +++ b/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs @@ -10,6 +10,7 @@ using Hangfire.Annotations; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Extensions.DependencyInjection; using Refit; using static Elsa.Api.Client.RefitSettingsHelper; @@ -68,6 +69,7 @@ public class WorkflowServer(Infrastructure infrastructure, string url) : WebAppl { management.UseEntityFrameworkCore(ef => ef.UsePostgreSql(dbConnectionString)); management.UseMassTransitDispatcher(); + management.UseCache(); }); elsa.UseWorkflowRuntime(runtime => {