1
0
mirror of https://github.com/bitwarden/server.git synced 2025-03-09 12:59:09 +01:00

[PM-14377] Add PATCH complete endpoint (#5100)

* Added CQRS pattern

* Added the GetManyByUserIdAsync signature to the repositiory

* Added sql sproc

Created user defined type to hold status

Created migration file

* Added ef core query

* Added absract and concrete implementation for GetManyByUserIdStatusAsync

* Added integration tests

* Updated params to status

* Implemented new query to utilize repository method

* Added controller for the security task endpoint

* Fixed lint issues

* Added documentation

* simplified to require single status

modified script to check for users with edit rights

* Updated ef core query

* Added new assertions

* simplified to require single status

* fixed formatting

* Fixed sql script

* Removed default null

* Added OperationAuthorizationRequirement for secruity task

* Added and registered MarkTaskAsCompletedCommand

* Added unit tests for the command

* Added complete endpoint

* removed false value
This commit is contained in:
SmithThe4th 2024-12-13 14:50:20 -05:00 committed by GitHub
parent c0a9c55891
commit 141a046a28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 207 additions and 2 deletions

View File

@ -3,6 +3,7 @@ using Bit.Api.Vault.Models.Response;
using Bit.Core;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Core.Vault.Commands.Interfaces;
using Bit.Core.Vault.Enums;
using Bit.Core.Vault.Queries;
using Microsoft.AspNetCore.Authorization;
@ -17,11 +18,16 @@ public class SecurityTaskController : Controller
{
private readonly IUserService _userService;
private readonly IGetTaskDetailsForUserQuery _getTaskDetailsForUserQuery;
private readonly IMarkTaskAsCompleteCommand _markTaskAsCompleteCommand;
public SecurityTaskController(IUserService userService, IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery)
public SecurityTaskController(
IUserService userService,
IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery,
IMarkTaskAsCompleteCommand markTaskAsCompleteCommand)
{
_userService = userService;
_getTaskDetailsForUserQuery = getTaskDetailsForUserQuery;
_markTaskAsCompleteCommand = markTaskAsCompleteCommand;
}
/// <summary>
@ -37,4 +43,15 @@ public class SecurityTaskController : Controller
var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList();
return new ListResponseModel<SecurityTasksResponseModel>(response);
}
/// <summary>
/// Marks a task as complete. The user must have edit permission on the cipher associated with the task.
/// </summary>
/// <param name="taskId">The unique identifier of the task to complete</param>
[HttpPatch("{taskId:guid}/complete")]
public async Task<IActionResult> Complete(Guid taskId)
{
await _markTaskAsCompleteCommand.CompleteAsync(taskId);
return NoContent();
}
}

View File

@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace Bit.Core.Vault.Authorization;
public class SecurityTaskOperationRequirement : OperationAuthorizationRequirement
{
public SecurityTaskOperationRequirement(string name)
{
Name = name;
}
}
public static class SecurityTaskOperations
{
public static readonly SecurityTaskOperationRequirement Update = new(nameof(Update));
}

View File

@ -0,0 +1,11 @@
namespace Bit.Core.Vault.Commands.Interfaces;
public interface IMarkTaskAsCompleteCommand
{
/// <summary>
/// Marks a task as complete.
/// </summary>
/// <param name="taskId">The unique identifier of the task to complete</param>
/// <returns>A task representing the async operation</returns>
Task CompleteAsync(Guid taskId);
}

View File

@ -0,0 +1,50 @@
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
using Bit.Core.Vault.Authorization;
using Bit.Core.Vault.Commands.Interfaces;
using Bit.Core.Vault.Enums;
using Bit.Core.Vault.Repositories;
using Microsoft.AspNetCore.Authorization;
namespace Bit.Core.Vault.Commands;
public class MarkTaskAsCompletedCommand : IMarkTaskAsCompleteCommand
{
private readonly ISecurityTaskRepository _securityTaskRepository;
private readonly IAuthorizationService _authorizationService;
private readonly ICurrentContext _currentContext;
public MarkTaskAsCompletedCommand(
ISecurityTaskRepository securityTaskRepository,
IAuthorizationService authorizationService,
ICurrentContext currentContext)
{
_securityTaskRepository = securityTaskRepository;
_authorizationService = authorizationService;
_currentContext = currentContext;
}
/// <inheritdoc />
public async Task CompleteAsync(Guid taskId)
{
if (!_currentContext.UserId.HasValue)
{
throw new NotFoundException();
}
var task = await _securityTaskRepository.GetByIdAsync(taskId);
if (task is null)
{
throw new NotFoundException();
}
await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, task,
SecurityTaskOperations.Update);
task.Status = SecurityTaskStatus.Completed;
task.RevisionDate = DateTime.UtcNow;
await _securityTaskRepository.ReplaceAsync(task);
}
}

View File

@ -1,4 +1,6 @@
using Bit.Core.Vault.Queries;
using Bit.Core.Vault.Commands;
using Bit.Core.Vault.Commands.Interfaces;
using Bit.Core.Vault.Queries;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.Vault;
@ -16,5 +18,6 @@ public static class VaultServiceCollectionExtensions
{
services.AddScoped<IOrganizationCiphersQuery, OrganizationCiphersQuery>();
services.AddScoped<IGetTaskDetailsForUserQuery, GetTaskDetailsForUserQuery>();
services.AddScoped<IMarkTaskAsCompleteCommand, MarkTaskAsCompletedCommand>();
}
}

View File

@ -0,0 +1,25 @@
using AutoFixture;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Enums;
using Bit.Test.Common.AutoFixture.Attributes;
namespace Bit.Core.Test.Vault.AutoFixture;
public class SecurityTaskFixtures : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customize<SecurityTask>(composer =>
composer
.With(task => task.Id, Guid.NewGuid())
.With(task => task.OrganizationId, Guid.NewGuid())
.With(task => task.Status, SecurityTaskStatus.Pending)
.Without(x => x.CipherId)
);
}
}
public class SecurityTaskCustomizeAttribute : BitCustomizeAttribute
{
public override ICustomization GetCustomization() => new SecurityTaskFixtures();
}

View File

@ -0,0 +1,83 @@
#nullable enable
using System.Security.Claims;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Test.Vault.AutoFixture;
using Bit.Core.Vault.Authorization;
using Bit.Core.Vault.Commands;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Vault.Commands;
[SutProviderCustomize]
[SecurityTaskCustomize]
public class MarkTaskAsCompletedCommandTest
{
private static void Setup(SutProvider<MarkTaskAsCompletedCommand> sutProvider, Guid taskId, SecurityTask? securityTask, Guid? userId, bool authorizedUpdate = false)
{
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ISecurityTaskRepository>()
.GetByIdAsync(taskId)
.Returns(securityTask);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), securityTask ?? Arg.Any<SecurityTask>(),
Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>
reqs.Contains(SecurityTaskOperations.Update)))
.Returns(authorizedUpdate ? AuthorizationResult.Success() : AuthorizationResult.Failed());
}
[Theory]
[BitAutoData]
public async Task CompleteAsync_NotLoggedIn_NotFoundException(
SutProvider<MarkTaskAsCompletedCommand> sutProvider,
Guid taskId,
SecurityTask securityTask)
{
Setup(sutProvider, taskId, securityTask, null, true);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CompleteAsync(taskId));
}
[Theory]
[BitAutoData]
public async Task CompleteAsync_TaskNotFound_NotFoundException(
SutProvider<MarkTaskAsCompletedCommand> sutProvider,
Guid taskId)
{
Setup(sutProvider, taskId, null, Guid.NewGuid(), true);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CompleteAsync(taskId));
}
[Theory]
[BitAutoData]
public async Task CompleteAsync_AuthorizationFailed_NotFoundException(
SutProvider<MarkTaskAsCompletedCommand> sutProvider,
Guid taskId,
SecurityTask securityTask)
{
Setup(sutProvider, taskId, securityTask, Guid.NewGuid());
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CompleteAsync(taskId));
}
[Theory]
[BitAutoData]
public async Task CompleteAsync_Success(
SutProvider<MarkTaskAsCompletedCommand> sutProvider,
Guid taskId,
SecurityTask securityTask)
{
Setup(sutProvider, taskId, securityTask, Guid.NewGuid(), true);
await sutProvider.Sut.CompleteAsync(taskId);
await sutProvider.GetDependency<ISecurityTaskRepository>().Received(1).ReplaceAsync(securityTask);
}
}