Skip to content

Commit 2bab069

Browse files
committed
fix(task-auth): allow project members to self-assign unassigned tasks (user role)
1 parent 487d0ce commit 2bab069

3 files changed

Lines changed: 112 additions & 2 deletions

File tree

src/TaskManagement.Api/Features/TaskItems/Commands/Handlers/PatchTaskItemCommandHandler.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,15 @@ public async Task<TaskItemDto> Handle(PatchTaskItemCommand request, Cancellation
6060
var isProjectOwner = taskItem.Project.OwnerUserId == currentUserId;
6161
var isAssignee = taskItem.AssignedUserId == currentUserId;
6262
var canManageAsProjectManager = isProjectManager && (isProjectOwner || isProjectMember);
63+
var isSelfAssigningUnassignedTaskOnly = IsSelfAssigningUnassignedTaskOnly(
64+
request,
65+
currentUserId,
66+
taskItem.AssignedUserId,
67+
isAdmin,
68+
isProjectManager,
69+
isProjectMember);
6370

64-
if (!isAdmin && !canManageAsProjectManager && !isProjectOwner && !isAssignee)
71+
if (!isAdmin && !canManageAsProjectManager && !isProjectOwner && !isAssignee && !isSelfAssigningUnassignedTaskOnly)
6572
{
6673
throw new ForbiddenAccessException("User is not authorized to update this task item.");
6774
}
@@ -188,6 +195,33 @@ public async Task<TaskItemDto> Handle(PatchTaskItemCommand request, Cancellation
188195
return assignedUserId.Trim();
189196
}
190197

198+
private static bool IsSelfAssigningUnassignedTaskOnly(
199+
PatchTaskItemCommand request,
200+
string currentUserId,
201+
string? currentAssignedUserId,
202+
bool isAdmin,
203+
bool isProjectManager,
204+
bool isProjectMember)
205+
{
206+
if (isAdmin || isProjectManager || !isProjectMember)
207+
{
208+
return false;
209+
}
210+
211+
if (!request.AssignedUserId.HasValue || !string.IsNullOrWhiteSpace(currentAssignedUserId))
212+
{
213+
return false;
214+
}
215+
216+
if (request.Title.HasValue || request.Description.HasValue || request.Status.HasValue || request.DueDate.HasValue)
217+
{
218+
return false;
219+
}
220+
221+
var normalizedRequestedAssignee = NormalizeAssignedUserId(request.AssignedUserId.Value);
222+
return string.Equals(normalizedRequestedAssignee, currentUserId, StringComparison.Ordinal);
223+
}
224+
191225
private static void EnsureAssignmentChangeAllowedForCurrentUser(
192226
string currentUserId,
193227
string? currentAssignedUserId,

src/TaskManagement.Api/Features/TaskItems/Commands/Handlers/UpdateTaskItemCommandHandler.cs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,15 @@ public async Task<TaskItemDto> Handle(UpdateTaskItemCommand request, Cancellatio
5959
bool isProjectOwner = taskItem.Project.OwnerUserId == currentUserId;
6060
bool isAssignee = taskItem.AssignedUserId == currentUserId;
6161
var canManageAsProjectManager = isProjectManager && (isProjectOwner || isProjectMember);
62+
var isSelfAssigningUnassignedTaskOnly = IsSelfAssigningUnassignedTaskOnly(
63+
request,
64+
taskItem,
65+
currentUserId,
66+
isAdmin,
67+
isProjectManager,
68+
isProjectMember);
6269

63-
if (!isAdmin && !canManageAsProjectManager && !isProjectOwner && !isAssignee)
70+
if (!isAdmin && !canManageAsProjectManager && !isProjectOwner && !isAssignee && !isSelfAssigningUnassignedTaskOnly)
6471
{
6572
throw new ForbiddenAccessException("User is not authorized to update this task item.");
6673
}
@@ -176,6 +183,36 @@ public async Task<TaskItemDto> Handle(UpdateTaskItemCommand request, Cancellatio
176183
return assignedUserId.Trim();
177184
}
178185

186+
private static bool IsSelfAssigningUnassignedTaskOnly(
187+
UpdateTaskItemCommand request,
188+
TaskItem taskItem,
189+
string currentUserId,
190+
bool isAdmin,
191+
bool isProjectManager,
192+
bool isProjectMember)
193+
{
194+
if (isAdmin || isProjectManager || !isProjectMember)
195+
{
196+
return false;
197+
}
198+
199+
if (!string.IsNullOrWhiteSpace(taskItem.AssignedUserId))
200+
{
201+
return false;
202+
}
203+
204+
var normalizedRequestedAssignee = NormalizeAssignedUserId(request.AssignedUserId);
205+
if (!string.Equals(normalizedRequestedAssignee, currentUserId, StringComparison.Ordinal))
206+
{
207+
return false;
208+
}
209+
210+
return string.Equals(request.Title, taskItem.Title, StringComparison.Ordinal)
211+
&& string.Equals(request.Description, taskItem.Description, StringComparison.Ordinal)
212+
&& request.Status == taskItem.Status
213+
&& request.DueDate == taskItem.DueDate;
214+
}
215+
179216
private static void EnsureAssignmentChangeAllowedForCurrentUser(
180217
string currentUserId,
181218
string? currentAssignedUserId,

tests/TaskManagement.Api.Tests/IntegrationTests/Features/TaskItems/PatchTaskItemEndpointTests.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public class PatchTaskItemEndpointTests : IClassFixture<ApiWebApplicationFactory
2323

2424
private readonly Guid _projectId = Guid.NewGuid();
2525
private readonly Guid _taskId = Guid.NewGuid();
26+
private readonly Guid _unassignedTaskId = Guid.NewGuid();
2627
private readonly string _ownerUserId = "user-task-patch-owner";
2728
private readonly string _memberUserId = "user-task-patch-member";
2829
private readonly string _otherUserId = "user-task-patch-other";
@@ -63,6 +64,15 @@ await _factory.SeedDatabaseAsync(db =>
6364
ProjectId = _projectId,
6465
AssignedUserId = _memberUserId
6566
});
67+
db.TaskItems.Add(new TaskItem
68+
{
69+
Id = _unassignedTaskId,
70+
Title = "Unassigned Task",
71+
Description = "No assignee yet",
72+
Status = TaskStatus.Todo,
73+
ProjectId = _projectId,
74+
AssignedUserId = null
75+
});
6676
return Task.CompletedTask;
6777
});
6878
}
@@ -188,6 +198,35 @@ public async Task PatchTaskItem_WhenUserAssignsTaskToAnotherUser_ShouldReturnFor
188198
response.StatusCode.Should().Be(HttpStatusCode.Forbidden);
189199
}
190200

201+
[Fact]
202+
public async Task PatchTaskItem_WhenProjectMemberSelfAssignsUnassignedTask_ShouldReturnOk()
203+
{
204+
SetAuthenticatedUser(_memberUserId);
205+
var command = new PatchTaskItemCommand { AssignedUserId = _memberUserId };
206+
207+
var response = await _client.PatchAsJsonAsync($"/api/taskitems/{_unassignedTaskId}", command);
208+
209+
response.StatusCode.Should().Be(HttpStatusCode.OK);
210+
var dto = await response.Content.ReadFromJsonAsync<TaskItemDto>();
211+
dto.Should().NotBeNull();
212+
dto!.AssignedUserId.Should().Be(_memberUserId);
213+
}
214+
215+
[Fact]
216+
public async Task PatchTaskItem_WhenProjectMemberSelfAssignsUnassignedTaskAndChangesOtherFields_ShouldReturnForbidden()
217+
{
218+
SetAuthenticatedUser(_memberUserId);
219+
var command = new PatchTaskItemCommand
220+
{
221+
AssignedUserId = _memberUserId,
222+
Title = "Should not be allowed"
223+
};
224+
225+
var response = await _client.PatchAsJsonAsync($"/api/taskitems/{_unassignedTaskId}", command);
226+
227+
response.StatusCode.Should().Be(HttpStatusCode.Forbidden);
228+
}
229+
191230
private void SetAuthenticatedUser(string userId, string? roles = null)
192231
{
193232
_client.DefaultRequestHeaders.Remove(TestAuthenticationHandler.TestUserIdHeader);

0 commit comments

Comments
 (0)