Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-14380] Add GET /tasks/organization endpoint #5149

Merged
merged 7 commits into from
Jan 9, 2025
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
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)
shane-melton marked this conversation as resolved.
Show resolved Hide resolved
{
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
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
Loading