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

Open
wants to merge 5 commits into
base: vault/pm-14378/security-task-auth-handler
Choose a base branch
from
Open
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 @@
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)

Check warning on line 28 in src/Api/Vault/Controllers/SecurityTaskController.cs

View check run for this annotation

Codecov / codecov/patch

src/Api/Vault/Controllers/SecurityTaskController.cs#L27-L28

Added lines #L27 - L28 were not covered by tests
{
_userService = userService;
_getTaskDetailsForUserQuery = getTaskDetailsForUserQuery;
_markTaskAsCompleteCommand = markTaskAsCompleteCommand;
_getTasksForOrganizationQuery = getTasksForOrganizationQuery;

Check warning on line 33 in src/Api/Vault/Controllers/SecurityTaskController.cs

View check run for this annotation

Codecov / codecov/patch

src/Api/Vault/Controllers/SecurityTaskController.cs#L33

Added line #L33 was not covered by tests
}

/// <summary>
Expand All @@ -54,4 +57,18 @@
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);
}

Check warning on line 73 in src/Api/Vault/Controllers/SecurityTaskController.cs

View check run for this annotation

Codecov / codecov/patch

src/Api/Vault/Controllers/SecurityTaskController.cs#L69-L73

Added lines #L69 - L73 were not covered by tests
}
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 @@

return results.ToList();
}

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

Check warning on line 40 in src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs

View check run for this annotation

Codecov / codecov/patch

src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs#L39-L40

Added lines #L39 - L40 were not covered by tests

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

Check warning on line 45 in src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs

View check run for this annotation

Codecov / codecov/patch

src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs#L42-L45

Added lines #L42 - L45 were not covered by tests

return results.ToList();
}

Check warning on line 48 in src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs

View check run for this annotation

Codecov / codecov/patch

src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs#L48

Added line #L48 was not covered by tests
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,31 @@
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,
};

Check warning on line 51 in src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs

View check run for this annotation

Codecov / codecov/patch

src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs#L32-L51

Added lines #L32 - L51 were not covered by tests

return await query.OrderByDescending(st => st.CreationDate).ToListAsync();
}

Check warning on line 54 in src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs

View check run for this annotation

Codecov / codecov/patch

src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs#L53-L54

Added lines #L53 - L54 were not covered by tests
}
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