mirror of
https://github.com/bitwarden/server.git
synced 2025-04-05 13:08:17 -05:00
[PM-14380] Add GET /tasks/organization endpoint (#5149)
* [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
This commit is contained in:
parent
a99f82dddd
commit
0605590ed2
@ -19,15 +19,18 @@ public class SecurityTaskController : Controller
|
|||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IGetTaskDetailsForUserQuery _getTaskDetailsForUserQuery;
|
private readonly IGetTaskDetailsForUserQuery _getTaskDetailsForUserQuery;
|
||||||
private readonly IMarkTaskAsCompleteCommand _markTaskAsCompleteCommand;
|
private readonly IMarkTaskAsCompleteCommand _markTaskAsCompleteCommand;
|
||||||
|
private readonly IGetTasksForOrganizationQuery _getTasksForOrganizationQuery;
|
||||||
|
|
||||||
public SecurityTaskController(
|
public SecurityTaskController(
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery,
|
IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery,
|
||||||
IMarkTaskAsCompleteCommand markTaskAsCompleteCommand)
|
IMarkTaskAsCompleteCommand markTaskAsCompleteCommand,
|
||||||
|
IGetTasksForOrganizationQuery getTasksForOrganizationQuery)
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_getTaskDetailsForUserQuery = getTaskDetailsForUserQuery;
|
_getTaskDetailsForUserQuery = getTaskDetailsForUserQuery;
|
||||||
_markTaskAsCompleteCommand = markTaskAsCompleteCommand;
|
_markTaskAsCompleteCommand = markTaskAsCompleteCommand;
|
||||||
|
_getTasksForOrganizationQuery = getTasksForOrganizationQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -54,4 +57,18 @@ public class SecurityTaskController : Controller
|
|||||||
await _markTaskAsCompleteCommand.CompleteAsync(taskId);
|
await _markTaskAsCompleteCommand.CompleteAsync(taskId);
|
||||||
return NoContent();
|
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
src/Core/Vault/Queries/GetTasksForOrganizationQuery.cs
Normal file
44
src/Core/Vault/Queries/GetTasksForOrganizationQuery.cs
Normal file
@ -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
src/Core/Vault/Queries/IGetTasksForOrganizationQuery.cs
Normal file
15
src/Core/Vault/Queries/IGetTasksForOrganizationQuery.cs
Normal file
@ -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);
|
||||||
|
}
|
@ -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>
|
/// <param name="status">Optional filter for task status. If not provided, returns tasks of all statuses</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<ICollection<SecurityTask>> GetManyByUserIdStatusAsync(Guid userId, SecurityTaskStatus? status = null);
|
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);
|
||||||
}
|
}
|
||||||
|
@ -32,4 +32,18 @@ public class SecurityTaskRepository : Repository<SecurityTask, Guid>, ISecurityT
|
|||||||
|
|
||||||
return results.ToList();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,4 +25,31 @@ public class SecurityTaskRepository : Repository<Core.Vault.Entities.SecurityTas
|
|||||||
var data = await query.Run(dbContext).ToListAsync();
|
var data = await query.Run(dbContext).ToListAsync();
|
||||||
return data;
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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…
x
Reference in New Issue
Block a user