Skip to content

Commit

Permalink
[PM-14380] Add GET /tasks/organization endpoint (#5149)
Browse files Browse the repository at this point in the history
* [PM-14380] Add GetManyByOrganizationIdStatusAsync to SecurityTaskRepository

* [PM-14380] Introduce IGetTasksForOrganizationQuery

* [PM-14380] Add /tasks/organization endpoint

* [PM-14380] Add unit tests

* [PM-14380] Formatting

* [PM-14380] Bump migration script date

* [PM-14380] Bump migration script date
  • Loading branch information
shane-melton authored Jan 9, 2025
1 parent a99f82d commit 0605590
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 1 deletion.
19 changes: 18 additions & 1 deletion src/Api/Vault/Controllers/SecurityTaskController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,18 @@ public class SecurityTaskController : Controller
private readonly IUserService _userService;
private readonly IGetTaskDetailsForUserQuery _getTaskDetailsForUserQuery;
private readonly IMarkTaskAsCompleteCommand _markTaskAsCompleteCommand;
private readonly IGetTasksForOrganizationQuery _getTasksForOrganizationQuery;

public SecurityTaskController(
IUserService userService,
IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery,
IMarkTaskAsCompleteCommand markTaskAsCompleteCommand)
IMarkTaskAsCompleteCommand markTaskAsCompleteCommand,
IGetTasksForOrganizationQuery getTasksForOrganizationQuery)
{
_userService = userService;
_getTaskDetailsForUserQuery = getTaskDetailsForUserQuery;
_markTaskAsCompleteCommand = markTaskAsCompleteCommand;
_getTasksForOrganizationQuery = getTasksForOrganizationQuery;
}

/// <summary>
Expand All @@ -54,4 +57,18 @@ public async Task<IActionResult> Complete(Guid taskId)
await _markTaskAsCompleteCommand.CompleteAsync(taskId);
return NoContent();
}

/// <summary>
/// Retrieves security tasks for an organization. Restricted to organization administrators.
/// </summary>
/// <param name="organizationId">The organization Id</param>
/// <param name="status">Optional filter for task status. If not provided, returns tasks of all statuses.</param>
[HttpGet("organization")]
public async Task<ListResponseModel<SecurityTasksResponseModel>> ListForOrganization(
[FromQuery] Guid organizationId, [FromQuery] SecurityTaskStatus? status)
{
var securityTasks = await _getTasksForOrganizationQuery.GetTasksAsync(organizationId, status);
var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList();
return new ListResponseModel<SecurityTasksResponseModel>(response);
}
}
44 changes: 44 additions & 0 deletions src/Core/Vault/Queries/GetTasksForOrganizationQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
using Bit.Core.Vault.Authorization.SecurityTasks;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Enums;
using Bit.Core.Vault.Repositories;
using Microsoft.AspNetCore.Authorization;

namespace Bit.Core.Vault.Queries;

public class GetTasksForOrganizationQuery : IGetTasksForOrganizationQuery
{
private readonly ISecurityTaskRepository _securityTaskRepository;
private readonly IAuthorizationService _authorizationService;
private readonly ICurrentContext _currentContext;

public GetTasksForOrganizationQuery(
ISecurityTaskRepository securityTaskRepository,
IAuthorizationService authorizationService,
ICurrentContext currentContext
)
{
_securityTaskRepository = securityTaskRepository;
_authorizationService = authorizationService;
_currentContext = currentContext;
}

public async Task<ICollection<SecurityTask>> GetTasksAsync(Guid organizationId,
SecurityTaskStatus? status = null)
{
var organization = _currentContext.GetOrganization(organizationId);
var userId = _currentContext.UserId;

if (organization == null || !userId.HasValue)
{
throw new NotFoundException();
}

await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, organization, SecurityTaskOperations.ListAllForOrganization);

return (await _securityTaskRepository.GetManyByOrganizationIdStatusAsync(organizationId, status)).ToList();
}
}
15 changes: 15 additions & 0 deletions src/Core/Vault/Queries/IGetTasksForOrganizationQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Enums;

namespace Bit.Core.Vault.Queries;

public interface IGetTasksForOrganizationQuery
{
/// <summary>
/// Retrieves all security tasks for an organization.
/// </summary>
/// <param name="organizationId">The Id of the organization</param>
/// <param name="status">Optional filter for task status. If not provided, returns tasks of all statuses</param>
/// <returns>A collection of security tasks</returns>
Task<ICollection<SecurityTask>> GetTasksAsync(Guid organizationId, SecurityTaskStatus? status = null);
}
8 changes: 8 additions & 0 deletions src/Core/Vault/Repositories/ISecurityTaskRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,12 @@ public interface ISecurityTaskRepository : IRepository<SecurityTask, Guid>
/// <param name="status">Optional filter for task status. If not provided, returns tasks of all statuses</param>
/// <returns></returns>
Task<ICollection<SecurityTask>> GetManyByUserIdStatusAsync(Guid userId, SecurityTaskStatus? status = null);

/// <summary>
/// Retrieves all security tasks for an organization.
/// </summary>
/// <param name="organizationId">The id of the organization</param>
/// <param name="status">Optional filter for task status. If not provided, returns tasks of all statuses</param>
/// <returns></returns>
Task<ICollection<SecurityTask>> GetManyByOrganizationIdStatusAsync(Guid organizationId, SecurityTaskStatus? status = null);
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,18 @@ public async Task<ICollection<SecurityTask>> GetManyByUserIdStatusAsync(Guid use

return results.ToList();
}

/// <inheritdoc />
public async Task<ICollection<SecurityTask>> GetManyByOrganizationIdStatusAsync(Guid organizationId,
SecurityTaskStatus? status = null)
{
await using var connection = new SqlConnection(ConnectionString);

var results = await connection.QueryAsync<SecurityTask>(
$"[{Schema}].[SecurityTask_ReadByOrganizationIdStatus]",
new { OrganizationId = organizationId, Status = status },
commandType: CommandType.StoredProcedure);

return results.ToList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,31 @@ public SecurityTaskRepository(IServiceScopeFactory serviceScopeFactory, IMapper
var data = await query.Run(dbContext).ToListAsync();
return data;
}

/// <inheritdoc />
public async Task<ICollection<Core.Vault.Entities.SecurityTask>> GetManyByOrganizationIdStatusAsync(Guid organizationId,
SecurityTaskStatus? status = null)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var query = from st in dbContext.SecurityTasks
join o in dbContext.Organizations
on st.OrganizationId equals o.Id
where
o.Enabled &&
st.OrganizationId == organizationId &&
(status == null || st.Status == status)
select new Core.Vault.Entities.SecurityTask
{
Id = st.Id,
OrganizationId = st.OrganizationId,
CipherId = st.CipherId,
Status = st.Status,
Type = st.Type,
CreationDate = st.CreationDate,
RevisionDate = st.RevisionDate,
};

return await query.OrderByDescending(st => st.CreationDate).ToListAsync();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
CREATE PROCEDURE [dbo].[SecurityTask_ReadByOrganizationIdStatus]
@OrganizationId UNIQUEIDENTIFIER,
@Status TINYINT = NULL
AS
BEGIN
SET NOCOUNT ON

SELECT
ST.*
FROM
[dbo].[SecurityTaskView] ST
INNER JOIN
[dbo].[Organization] O ON O.[Id] = ST.[OrganizationId]
WHERE
ST.[OrganizationId] = @OrganizationId
AND O.[Enabled] = 1
AND ST.[Status] = COALESCE(@Status, ST.[Status])
ORDER BY ST.[CreationDate] DESC
END
92 changes: 92 additions & 0 deletions test/Core.Test/Vault/Queries/GetTasksForOrganizationQueryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System.Security.Claims;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Vault.Authorization.SecurityTasks;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Enums;
using Bit.Core.Vault.Queries;
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.Queries;

[SutProviderCustomize]
public class GetTasksForOrganizationQueryTests
{
[Theory, BitAutoData]
public async Task GetTasksAsync_Success(
Guid userId, CurrentContextOrganization org,
SutProvider<GetTasksForOrganizationQuery> sutProvider)
{
var status = SecurityTaskStatus.Pending;
sutProvider.GetDependency<ICurrentContext>().HttpContext.User.Returns(new ClaimsPrincipal());
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(org.Id).Returns(org);
sutProvider.GetDependency<IAuthorizationService>().AuthorizeAsync(
Arg.Any<ClaimsPrincipal>(), org, Arg.Is<IEnumerable<IAuthorizationRequirement>>(
e => e.Contains(SecurityTaskOperations.ListAllForOrganization)
)
).Returns(AuthorizationResult.Success());
sutProvider.GetDependency<ISecurityTaskRepository>().GetManyByOrganizationIdStatusAsync(org.Id, status).Returns(new List<SecurityTask>()
{
new() { Id = Guid.NewGuid() },
new() { Id = Guid.NewGuid() },
});

var result = await sutProvider.Sut.GetTasksAsync(org.Id, status);

Assert.Equal(2, result.Count);
sutProvider.GetDependency<IAuthorizationService>().Received(1).AuthorizeAsync(
Arg.Any<ClaimsPrincipal>(), org, Arg.Is<IEnumerable<IAuthorizationRequirement>>(
e => e.Contains(SecurityTaskOperations.ListAllForOrganization)
)
);
sutProvider.GetDependency<ISecurityTaskRepository>().Received(1).GetManyByOrganizationIdStatusAsync(org.Id, SecurityTaskStatus.Pending);
}

[Theory, BitAutoData]
public async Task GetTaskAsync_MissingOrg_Failure(Guid userId, SutProvider<GetTasksForOrganizationQuery> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);

await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetTasksAsync(Guid.NewGuid()));
}

[Theory, BitAutoData]
public async Task GetTaskAsync_MissingUser_Failure(CurrentContextOrganization org, SutProvider<GetTasksForOrganizationQuery> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(null as Guid?);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(org.Id).Returns(org);

await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetTasksAsync(org.Id));
}

[Theory, BitAutoData]
public async Task GetTasksAsync_Unauthorized_Failure(
Guid userId, CurrentContextOrganization org,
SutProvider<GetTasksForOrganizationQuery> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().HttpContext.User.Returns(new ClaimsPrincipal());
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(org.Id).Returns(org);
sutProvider.GetDependency<IAuthorizationService>().AuthorizeAsync(
Arg.Any<ClaimsPrincipal>(), org, Arg.Is<IEnumerable<IAuthorizationRequirement>>(
e => e.Contains(SecurityTaskOperations.ListAllForOrganization)
)
).Returns(AuthorizationResult.Failed());

await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetTasksAsync(org.Id));

sutProvider.GetDependency<IAuthorizationService>().Received(1).AuthorizeAsync(
Arg.Any<ClaimsPrincipal>(), org, Arg.Is<IEnumerable<IAuthorizationRequirement>>(
e => e.Contains(SecurityTaskOperations.ListAllForOrganization)
)
);
sutProvider.GetDependency<ISecurityTaskRepository>().Received(0).GetManyByOrganizationIdStatusAsync(org.Id, SecurityTaskStatus.Pending);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
CREATE OR ALTER PROCEDURE [dbo].[SecurityTask_ReadByOrganizationIdStatus]
@OrganizationId UNIQUEIDENTIFIER,
@Status TINYINT = NULL
AS
BEGIN
SET NOCOUNT ON

SELECT
ST.*
FROM
[dbo].[SecurityTaskView] ST
INNER JOIN
[dbo].[Organization] O ON O.[Id] = ST.[OrganizationId]
WHERE
ST.[OrganizationId] = @OrganizationId
AND O.[Enabled] = 1
AND ST.[Status] = COALESCE(@Status, ST.[Status])
ORDER BY ST.[CreationDate] DESC
END
GO

0 comments on commit 0605590

Please sign in to comment.