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 => {