Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions UvA.Workflow.Api/Actions/ActionsController.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using UvA.Workflow.Api.Actions.Dtos;
using UvA.Workflow.Api.Infrastructure;
using UvA.Workflow.Api.WorkflowInstances;
using UvA.Workflow.Api.WorkflowInstances.Dtos;

namespace UvA.Workflow.Api.Actions;

public class ActionsController(
IWorkflowInstanceRepository workflowInstanceRepository,
IUserService userService,
RightsService rightsService,
TriggerService triggerService,
WorkflowInstanceDtoFactory workflowInstanceDtoFactory,
Expand All @@ -17,6 +17,10 @@ InstanceService instanceService
public async Task<ActionResult<ExecuteActionPayloadDto>> ExecuteAction([FromBody] ExecuteActionInputDto input,
CancellationToken ct)
{
var currentUser = await userService.GetCurrentUser(ct);
if (currentUser == null)
return Unauthorized();

var instance = await workflowInstanceRepository.GetById(input.InstanceId, ct);
if (instance == null)
return WorkflowInstanceNotFound;
Expand All @@ -38,7 +42,7 @@ public async Task<ActionResult<ExecuteActionPayloadDto>> ExecuteAction([FromBody
if (action == null)
return Forbidden();

await triggerService.RunTriggers(instance, action.Triggers, ct, input.Mail);
await triggerService.RunTriggers(instance, action.Triggers, currentUser, ct, input.Mail);
await instanceService.UpdateCurrentStep(instance, ct);
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ namespace UvA.Workflow.Api.Authentication;

public class SurfConextAuthenticationHandler : AuthenticationHandler<SurfConextOptions>
{
private const string SURFCONEXT_ERROR = "SurfConextError";
public static string Scheme => "SURFconext";
private const string SurfconextError = "SurfConextError";
public static string SchemeName => "SURFconext";

/// <summary>
/// implements the behavior of the SurfConext scheme to authenticate users.
Expand Down Expand Up @@ -56,7 +56,7 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
var cacheKey = $"bt_{bearerToken}";

if (cache.TryGetValue(cacheKey, out ClaimsPrincipal? cachedPrincipal))
return AuthenticateResult.Success(new AuthenticationTicket(cachedPrincipal!, Scheme));
return AuthenticateResult.Success(new AuthenticationTicket(cachedPrincipal!, SchemeName));

var resp = await ValidateSurfBearerToken(bearerToken);
if (resp == null)
Expand All @@ -81,7 +81,7 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()

await userService.AddOrUpdateUser(principal.Identity!.Name!, resp.FullName, resp.Email);

return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme));
return AuthenticateResult.Success(new AuthenticationTicket(principal, SchemeName));
}

protected override Task HandleChallengeAsync(AuthenticationProperties properties)
Expand All @@ -91,7 +91,7 @@ protected override Task HandleChallengeAsync(AuthenticationProperties properties
{
Status = StatusCodes.Status401Unauthorized,
Title = "Unauthorized",
Detail = Context.Items[SURFCONEXT_ERROR] as string ?? "Unauthorized",
Detail = Context.Items[SurfconextError] as string ?? "Unauthorized",
Instance = Context.Request.Path.Value
},
new JsonSerializerOptions(JsonSerializerDefaults.Web));
Expand All @@ -112,7 +112,7 @@ protected override Task HandleChallengeAsync(AuthenticationProperties properties
"Token validation failed: SurfConext returned status {Code}: {Response}, ClientId:{ClientId}, Secret:{ClientSecret}",
response.StatusCode, content, OptionsMonitor.CurrentValue.ClientId,
OptionsMonitor.CurrentValue.ClientSecret?[..4]);
Context.Items[SURFCONEXT_ERROR] =
Context.Items[SurfconextError] =
$"Token validation failed: SurfConext returned status {response.StatusCode}, check the logs for details";
return null;
}
Expand All @@ -124,7 +124,7 @@ protected override Task HandleChallengeAsync(AuthenticationProperties properties
catch (Exception ex)
{
Logger.LogError(ex, "Token validation failed: unable to deserialize response: {Response}", content);
Context.Items[SURFCONEXT_ERROR] =
Context.Items[SurfconextError] =
$"Token validation failed: unable to deserialize response from SurfConext, check the logs for details";
return null;
}
Expand All @@ -148,7 +148,7 @@ private static ClaimsPrincipal CreateClaimsPrincipal(IntrospectionResponse r)

if (r.Uids is { Length: > 0 } && !string.IsNullOrWhiteSpace(r.Uids[0]))
{
claims.Add(new Claim(ClaimTypes.NameIdentifier, r.Uids[0]));
claims.Add(new Claim(ClaimTypes.NameIdentifier, UvaClaimTypes.UvanetId));
claims.Add(new Claim(UvaClaimTypes.UvanetId, r.Uids[0]));
}

Expand Down Expand Up @@ -177,7 +177,7 @@ private static ClaimsPrincipal CreateClaimsPrincipal(IntrospectionResponse r)
claims.Add(new Claim("updated_at",
r.UpdatedAt.Value.ToString(System.Globalization.CultureInfo.InvariantCulture)));

var identity = new ClaimsIdentity(claims, Scheme, UvaClaimTypes.UvanetId, ClaimTypes.Role);
var identity = new ClaimsIdentity(claims, SchemeName, UvaClaimTypes.UvanetId, ClaimTypes.Role);
return new ClaimsPrincipal(identity);
}
}
3 changes: 2 additions & 1 deletion UvA.Workflow.Api/Authentication/SurfConextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public static IServiceCollection AddSurfConextAuthentication(this IServiceCollec

services.AddAuthentication(authOptions =>
{
authOptions.AddScheme<SurfConextAuthenticationHandler>(SurfConextAuthenticationHandler.Scheme, null);
authOptions.AddScheme<SurfConextAuthenticationHandler>(SurfConextAuthenticationHandler.SchemeName,
null);
});

services.AddSwaggerGen(c =>
Expand Down
12 changes: 10 additions & 2 deletions UvA.Workflow.Api/Events/EventsController.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
using UvA.Workflow.Api.Infrastructure;
using UvA.Workflow.Events;

namespace UvA.Workflow.Api.Events;

[Route("/WorkflowInstances/{instanceId}/Events")]
public class EventsController(IWorkflowInstanceRepository workflowRepository, InstanceService instanceService)
public class EventsController(
IWorkflowInstanceRepository workflowRepository,
IUserService userService,
IInstanceEventService eventService)
: ApiControllerBase
{
[HttpDelete]
[Route("{eventName}")]
public async Task<IActionResult> DeleteEvent(string instanceId, string eventName, CancellationToken ct)
{
var user = await userService.GetCurrentUser(ct);
if (user == null)
return Unauthorized();

var instance = await workflowRepository.GetById(instanceId, ct);
if (instance == null)
return WorkflowInstanceNotFound;
await instanceService.DeleteEvent(instance, eventName, ct);
await eventService.DeleteEvent(instance, eventName, user, ct);
return Ok();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using UvA.Workflow.Api.Screens;
using UvA.Workflow.Api.Submissions.Dtos;
using UvA.Workflow.Events;
using UvA.Workflow.Infrastructure.Database;
using UvA.Workflow.Persistence;
using UvA.Workflow.Submissions;
Expand All @@ -26,6 +27,7 @@ public static IServiceCollection AddWorkflow(this IServiceCollection services, I
// Register repositories - organized by domain feature
services.AddScoped<IWorkflowInstanceRepository, WorkflowInstanceRepository>();
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IInstanceEventRepository, InstanceEventRepository>();

services.AddScoped<WorkflowInstanceService>();
services.AddScoped<IUserService, UserService>();
Expand All @@ -39,6 +41,8 @@ public static IServiceCollection AddWorkflow(this IServiceCollection services, I
services.AddScoped<AnswerDtoFactory>();

services.AddScoped<InstanceService>();
services.AddScoped<IInstanceEventService, InstanceEventService>();

services.AddScoped<RightsService>();
services.AddScoped<TriggerService>();
services.AddScoped<AnswerConversionService>();
Expand Down
1 change: 1 addition & 0 deletions UvA.Workflow.Api/Screens/ScreenDataService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using UvA.Workflow.Api.Screens.Dtos;
using UvA.Workflow.Events;
using UvA.Workflow.Tools;

namespace UvA.Workflow.Api.Screens;
Expand Down
1 change: 1 addition & 0 deletions UvA.Workflow.Api/Submissions/Dtos/SubmissionDto.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using UvA.Workflow.Api.Infrastructure;
using UvA.Workflow.Events;
using UvA.Workflow.Submissions;

namespace UvA.Workflow.Api.Submissions.Dtos;
Expand Down
9 changes: 7 additions & 2 deletions UvA.Workflow.Api/Submissions/SubmissionsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
namespace UvA.Workflow.Api.Submissions;

public class SubmissionsController(
IUserService userService,
ModelService modelService,
SubmissionService submissionService,
SubmissionDtoFactory submissionDtoFactory,
Expand All @@ -26,15 +27,19 @@ public async Task<ActionResult<SubmissionDto>> GetSubmission(string instanceId,
public async Task<ActionResult<SubmitSubmissionResult>> SubmitSubmission(string instanceId, string submissionId,
CancellationToken ct)
{
var currentUser = await userService.GetCurrentUser(ct);
if (currentUser == null)
return Unauthorized();
var context = await submissionService.GetSubmissionContext(instanceId, submissionId, ct);
var (instance, sub, form, _) = context;
var result = await submissionService.SubmitSubmission(context, ct);
var result = await submissionService.SubmitSubmission(context, currentUser, ct);

if (!result.Success)
{
var submissionDto = submissionDtoFactory.Create(instance, form, sub,
modelService.GetQuestionStatus(instance, form, true));
return Ok(new SubmitSubmissionResult(submissionDto, null, result.Errors, false));

return UnprocessableEntity(new SubmitSubmissionResult(submissionDto, null, result.Errors, false));
}

var finalSubmissionDto = submissionDtoFactory.Create(instance, form, instance.Events[submissionId],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using UvA.Workflow.Api.Actions.Dtos;
using UvA.Workflow.Api.EntityTypes.Dtos;
using UvA.Workflow.Api.Submissions.Dtos;
using UvA.Workflow.Events;

namespace UvA.Workflow.Api.WorkflowInstances.Dtos;

Expand Down
1 change: 1 addition & 0 deletions UvA.Workflow.Tests/Builders/EventBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using UvA.Workflow.Events;
using UvA.Workflow.WorkflowInstances;

namespace UvA.Workflow.Tests;
Expand Down
1 change: 1 addition & 0 deletions UvA.Workflow.Tests/Builders/WorkflowInstanceBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MongoDB.Bson;
using UvA.Workflow.Events;
using UvA.Workflow.WorkflowInstances;

namespace UvA.Workflow.Tests;
Expand Down
25 changes: 16 additions & 9 deletions UvA.Workflow.Tests/WorkflowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using MongoDB.Driver;
using Moq;
using UvA.Workflow.Entities.Domain;
using UvA.Workflow.Events;
using UvA.Workflow.Persistence;
using UvA.Workflow.Services;
using UvA.Workflow.Submissions;
Expand All @@ -14,13 +15,15 @@ namespace UvA.Workflow.Tests;

public class WorkflowTests
{
Mock<IWorkflowInstanceRepository> repoMock;
Mock<IWorkflowInstanceRepository> instanceRepoMock;
Mock<IInstanceEventRepository> eventRepoMock;
Mock<IUserService> userServiceMock;
Mock<IMailService> mailServiceMock;
Mock<IArtifactService> artifactServiceMock;
ModelService modelService;
RightsService rightsService;
InstanceService instanceService;
InstanceEventService eventService;
TriggerService triggerService;
SubmissionService submissionService;
ModelParser parser;
Expand All @@ -31,7 +34,8 @@ public class WorkflowTests
public WorkflowTests()
{
// Mocks
repoMock = new Mock<IWorkflowInstanceRepository>();
instanceRepoMock = new Mock<IWorkflowInstanceRepository>();
eventRepoMock = new Mock<IInstanceEventRepository>();
userServiceMock = new Mock<IUserService>();
mailServiceMock = new Mock<IMailService>();
artifactServiceMock = new Mock<IArtifactService>();
Expand All @@ -41,9 +45,12 @@ public WorkflowTests()
parser = new ModelParser(modelProvider);
modelService = new ModelService(parser);
rightsService = new RightsService(modelService, userServiceMock.Object);
instanceService = new InstanceService(repoMock.Object, modelService, userServiceMock.Object, rightsService);
triggerService = new TriggerService(instanceService, modelService, mailServiceMock.Object);
submissionService = new SubmissionService(repoMock.Object, modelService, triggerService, instanceService);
instanceService =
new InstanceService(instanceRepoMock.Object, modelService, userServiceMock.Object, rightsService);
eventService = new InstanceEventService(eventRepoMock.Object, rightsService, instanceService);
triggerService = new TriggerService(instanceService, eventService, modelService, mailServiceMock.Object);
submissionService =
new SubmissionService(instanceRepoMock.Object, modelService, triggerService, instanceService);
answerConversionService = new AnswerConversionService(userServiceMock.Object);
answerService = new AnswerService(submissionService, modelService, instanceService, rightsService,
artifactServiceMock.Object, answerConversionService);
Expand Down Expand Up @@ -87,7 +94,7 @@ public async Task SubmitForm_FillAnswer_Success()
)
.Build();

repoMock.Setup(r => r.GetById(instance.Id, It.IsAny<CancellationToken>())).ReturnsAsync(instance);
instanceRepoMock.Setup(r => r.GetById(instance.Id, It.IsAny<CancellationToken>())).ReturnsAsync(instance);
JsonElement value = JsonDocument.Parse("\"title\"").RootElement;

// Act
Expand All @@ -96,7 +103,7 @@ public async Task SubmitForm_FillAnswer_Success()

// Assert
Assert.Contains(instance.Properties, p => p.Key == "Title" && p.Value.ToString() == "title");
repoMock.Verify(
instanceRepoMock.Verify(
r => r.UpdateFields(instance.Id, It.IsAny<UpdateDefinition<WorkflowInstance>>(),
It.IsAny<CancellationToken>()), Times.Once);
}
Expand All @@ -117,7 +124,7 @@ public async Task SubmitForm_UploadArtifact_Success()
await writer.FlushAsync(ct);
const string fileName = "test.pdf";

repoMock.Setup(r => r.GetById(instance.Id, It.IsAny<CancellationToken>())).ReturnsAsync(instance);
instanceRepoMock.Setup(r => r.GetById(instance.Id, It.IsAny<CancellationToken>())).ReturnsAsync(instance);
artifactServiceMock.Setup(a => a.SaveArtifact(fileName, It.IsAny<Stream>()))
.ReturnsAsync(new ArtifactInfo(ObjectId.GenerateNewId(), fileName));

Expand All @@ -130,7 +137,7 @@ public async Task SubmitForm_UploadArtifact_Success()
var report = BsonSerializer.Deserialize<ArtifactInfo>(instance.Properties["Report"].ToBsonDocument());
Assert.Equal(fileName, report.Name);
artifactServiceMock.Verify(a => a.SaveArtifact(fileName, It.IsAny<Stream>()), Times.Once);
repoMock.Verify(
instanceRepoMock.Verify(
r => r.UpdateFields(instance.Id, It.IsAny<UpdateDefinition<WorkflowInstance>>(),
It.IsAny<CancellationToken>()), Times.Once);
}
Expand Down
27 changes: 27 additions & 0 deletions UvA.Workflow/Events/IInstanceEventRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace UvA.Workflow.Events;

public interface IInstanceEventRepository
{
/// <summary>
/// Adds a new event to a workflow instance or updates an existing event if it already exists.
/// Logs the operation specifying whether it was an addition or update.
/// </summary>
/// <param name="instance">The workflow instance in which the event should be added or updated.</param>
/// <param name="newEvent">The new event to add or the existing event to update.</param>
/// <param name="user">The user initiating the add or update operation.</param>
/// <param name="ct">The cancellation token used to observe the operation's cancellation.</param>
/// <returns>An asynchronous operation representing the add or update process.</returns>
Task AddOrUpdateEvent(WorkflowInstance instance, InstanceEvent newEvent, User user,
CancellationToken ct);

/// <summary>
/// Deletes a specified event from a workflow instance and logs the deletion.
/// </summary>
/// <param name="instance">The workflow instance from which the event is to be deleted.</param>
/// <param name="eventToDelete">The event to remove from the instance.</param>
/// <param name="user">The user executing the deletion action.</param>
/// <param name="ct">The cancellation token used to observe the operation's cancellation.</param>
/// <returns>An asynchronous operation representing the deletion process.</returns>
Task DeleteEvent(WorkflowInstance instance, InstanceEvent eventToDelete, User user,
CancellationToken ct);
}
7 changes: 7 additions & 0 deletions UvA.Workflow/Events/InstanceEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace UvA.Workflow.Events;

public class InstanceEvent
{
public string Id { get; set; } = null!;
public DateTime? Date { get; set; }
}
38 changes: 38 additions & 0 deletions UvA.Workflow/Events/InstanceEventLogEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using MongoDB.Bson.Serialization.Attributes;

namespace UvA.Workflow.Events;

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum EventLogOperation
{
Create,
Update,
Delete
}

public class InstanceEventLogEntry
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; } = null!;

[BsonElement("Timestamp")] public DateTime Timestamp { get; set; } = DateTime.UtcNow;

[BsonElement("InstanceId")]
[BsonRepresentation(BsonType.ObjectId)]
public string WorkflowInstanceId { get; set; } = null!;

[BsonElement("EventId")] public string EventId { get; set; } = null!;

[BsonElement("ExecutedBy")]
[BsonRepresentation(BsonType.ObjectId)]
public string ExecutedBy { get; set; } = null!;

[BsonElement("EventDate")]
[BsonDateTimeOptions(Kind = DateTimeKind.Utc)]
public DateTime? EventDate { get; set; }

[BsonRepresentation(BsonType.String)]
[BsonElement("Operation")]
public EventLogOperation Operation { get; set; }
}
Loading