mirror of
https://github.com/bitwarden/server.git
synced 2025-04-04 20:50:21 -05:00
[PM-14376] Add GET tasks endpoint (#5089)
* 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 security tasks feature flag
This commit is contained in:
parent
03dde0d008
commit
a332a69112
40
src/Api/Vault/Controllers/SecurityTaskController.cs
Normal file
40
src/Api/Vault/Controllers/SecurityTaskController.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
using Bit.Api.Models.Response;
|
||||||
|
using Bit.Api.Vault.Models.Response;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Core.Vault.Enums;
|
||||||
|
using Bit.Core.Vault.Queries;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bit.Api.Vault.Controllers;
|
||||||
|
|
||||||
|
[Route("tasks")]
|
||||||
|
[Authorize("Application")]
|
||||||
|
[RequireFeature(FeatureFlagKeys.SecurityTasks)]
|
||||||
|
public class SecurityTaskController : Controller
|
||||||
|
{
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly IGetTaskDetailsForUserQuery _getTaskDetailsForUserQuery;
|
||||||
|
|
||||||
|
public SecurityTaskController(IUserService userService, IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery)
|
||||||
|
{
|
||||||
|
_userService = userService;
|
||||||
|
_getTaskDetailsForUserQuery = getTaskDetailsForUserQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves security tasks for the current user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="status">Optional filter for task status. If not provided returns tasks of all statuses.</param>
|
||||||
|
/// <returns>A list response model containing the security tasks for the user.</returns>
|
||||||
|
[HttpGet("")]
|
||||||
|
public async Task<ListResponseModel<SecurityTasksResponseModel>> Get([FromQuery] SecurityTaskStatus? status)
|
||||||
|
{
|
||||||
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
|
var securityTasks = await _getTaskDetailsForUserQuery.GetTaskDetailsForUserAsync(userId, status);
|
||||||
|
var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList();
|
||||||
|
return new ListResponseModel<SecurityTasksResponseModel>(response);
|
||||||
|
}
|
||||||
|
}
|
30
src/Api/Vault/Models/Response/SecurityTasksResponseModel.cs
Normal file
30
src/Api/Vault/Models/Response/SecurityTasksResponseModel.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using Bit.Core.Models.Api;
|
||||||
|
using Bit.Core.Vault.Entities;
|
||||||
|
using Bit.Core.Vault.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Api.Vault.Models.Response;
|
||||||
|
|
||||||
|
public class SecurityTasksResponseModel : ResponseModel
|
||||||
|
{
|
||||||
|
public SecurityTasksResponseModel(SecurityTask securityTask, string obj = "securityTask")
|
||||||
|
: base(obj)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(securityTask);
|
||||||
|
|
||||||
|
Id = securityTask.Id;
|
||||||
|
OrganizationId = securityTask.OrganizationId;
|
||||||
|
CipherId = securityTask.CipherId;
|
||||||
|
Type = securityTask.Type;
|
||||||
|
Status = securityTask.Status;
|
||||||
|
CreationDate = securityTask.CreationDate;
|
||||||
|
RevisionDate = securityTask.RevisionDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
|
public Guid? CipherId { get; set; }
|
||||||
|
public SecurityTaskType Type { get; set; }
|
||||||
|
public SecurityTaskStatus Status { get; set; }
|
||||||
|
public DateTime CreationDate { get; set; }
|
||||||
|
public DateTime RevisionDate { get; set; }
|
||||||
|
}
|
13
src/Core/Vault/Queries/GetTaskDetailsForUserQuery.cs
Normal file
13
src/Core/Vault/Queries/GetTaskDetailsForUserQuery.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using Bit.Core.Vault.Entities;
|
||||||
|
using Bit.Core.Vault.Enums;
|
||||||
|
using Bit.Core.Vault.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Core.Vault.Queries;
|
||||||
|
|
||||||
|
public class GetTaskDetailsForUserQuery(ISecurityTaskRepository securityTaskRepository) : IGetTaskDetailsForUserQuery
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IEnumerable<SecurityTask>> GetTaskDetailsForUserAsync(Guid userId,
|
||||||
|
SecurityTaskStatus? status = null)
|
||||||
|
=> await securityTaskRepository.GetManyByUserIdStatusAsync(userId, status);
|
||||||
|
}
|
15
src/Core/Vault/Queries/IGetTaskDetailsForUserQuery.cs
Normal file
15
src/Core/Vault/Queries/IGetTaskDetailsForUserQuery.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using Bit.Core.Vault.Entities;
|
||||||
|
using Bit.Core.Vault.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Vault.Queries;
|
||||||
|
|
||||||
|
public interface IGetTaskDetailsForUserQuery
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves security tasks for a user based on their organization and cipher access permissions.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The Id of the user retrieving tasks</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<IEnumerable<SecurityTask>> GetTaskDetailsForUserAsync(Guid userId, SecurityTaskStatus? status = null);
|
||||||
|
}
|
@ -1,9 +1,16 @@
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
|
using Bit.Core.Vault.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.Vault.Repositories;
|
namespace Bit.Core.Vault.Repositories;
|
||||||
|
|
||||||
public interface ISecurityTaskRepository : IRepository<SecurityTask, Guid>
|
public interface ISecurityTaskRepository : IRepository<SecurityTask, Guid>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves security tasks for a user based on their organization and cipher access permissions.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The Id of the user retrieving tasks</param>
|
||||||
|
/// <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);
|
||||||
}
|
}
|
||||||
|
@ -15,5 +15,6 @@ public static class VaultServiceCollectionExtensions
|
|||||||
private static void AddVaultQueries(this IServiceCollection services)
|
private static void AddVaultQueries(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<IOrganizationCiphersQuery, OrganizationCiphersQuery>();
|
services.AddScoped<IOrganizationCiphersQuery, OrganizationCiphersQuery>();
|
||||||
|
services.AddScoped<IGetTaskDetailsForUserQuery, GetTaskDetailsForUserQuery>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
using Bit.Core.Settings;
|
using System.Data;
|
||||||
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
|
using Bit.Core.Vault.Enums;
|
||||||
using Bit.Core.Vault.Repositories;
|
using Bit.Core.Vault.Repositories;
|
||||||
using Bit.Infrastructure.Dapper.Repositories;
|
using Bit.Infrastructure.Dapper.Repositories;
|
||||||
|
using Dapper;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
|
||||||
namespace Bit.Infrastructure.Dapper.Vault.Repositories;
|
namespace Bit.Infrastructure.Dapper.Vault.Repositories;
|
||||||
|
|
||||||
@ -15,4 +19,17 @@ public class SecurityTaskRepository : Repository<SecurityTask, Guid>, ISecurityT
|
|||||||
: base(connectionString, readOnlyConnectionString)
|
: base(connectionString, readOnlyConnectionString)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<ICollection<SecurityTask>> GetManyByUserIdStatusAsync(Guid userId,
|
||||||
|
SecurityTaskStatus? status = null)
|
||||||
|
{
|
||||||
|
await using var connection = new SqlConnection(ConnectionString);
|
||||||
|
|
||||||
|
var results = await connection.QueryAsync<SecurityTask>(
|
||||||
|
$"[{Schema}].[SecurityTask_ReadByUserIdStatus]",
|
||||||
|
new { UserId = userId, Status = status },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
return results.ToList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,90 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Vault.Entities;
|
||||||
|
using Bit.Core.Vault.Enums;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.EntityFramework.Vault.Repositories.Queries;
|
||||||
|
|
||||||
|
public class SecurityTaskReadByUserIdStatusQuery : IQuery<SecurityTask>
|
||||||
|
{
|
||||||
|
private readonly Guid _userId;
|
||||||
|
private readonly SecurityTaskStatus? _status;
|
||||||
|
|
||||||
|
public SecurityTaskReadByUserIdStatusQuery(Guid userId, SecurityTaskStatus? status)
|
||||||
|
{
|
||||||
|
_userId = userId;
|
||||||
|
_status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IQueryable<SecurityTask> Run(DatabaseContext dbContext)
|
||||||
|
{
|
||||||
|
var query = from st in dbContext.SecurityTasks
|
||||||
|
|
||||||
|
join ou in dbContext.OrganizationUsers
|
||||||
|
on st.OrganizationId equals ou.OrganizationId
|
||||||
|
|
||||||
|
join o in dbContext.Organizations
|
||||||
|
on st.OrganizationId equals o.Id
|
||||||
|
|
||||||
|
join c in dbContext.Ciphers
|
||||||
|
on st.CipherId equals c.Id into c_g
|
||||||
|
from c in c_g.DefaultIfEmpty()
|
||||||
|
|
||||||
|
join cc in dbContext.CollectionCiphers
|
||||||
|
on c.Id equals cc.CipherId into cc_g
|
||||||
|
from cc in cc_g.DefaultIfEmpty()
|
||||||
|
|
||||||
|
join cu in dbContext.CollectionUsers
|
||||||
|
on new { cc.CollectionId, OrganizationUserId = ou.Id } equals
|
||||||
|
new { cu.CollectionId, cu.OrganizationUserId } into cu_g
|
||||||
|
from cu in cu_g.DefaultIfEmpty()
|
||||||
|
|
||||||
|
join gu in dbContext.GroupUsers
|
||||||
|
on new { CollectionId = (Guid?)cu.CollectionId, OrganizationUserId = ou.Id } equals
|
||||||
|
new { CollectionId = (Guid?)null, gu.OrganizationUserId } into gu_g
|
||||||
|
from gu in gu_g.DefaultIfEmpty()
|
||||||
|
|
||||||
|
join cg in dbContext.CollectionGroups
|
||||||
|
on new { cc.CollectionId, gu.GroupId } equals
|
||||||
|
new { cg.CollectionId, cg.GroupId } into cg_g
|
||||||
|
from cg in cg_g.DefaultIfEmpty()
|
||||||
|
|
||||||
|
where
|
||||||
|
ou.UserId == _userId &&
|
||||||
|
ou.Status == OrganizationUserStatusType.Confirmed &&
|
||||||
|
o.Enabled &&
|
||||||
|
(
|
||||||
|
st.CipherId == null ||
|
||||||
|
(
|
||||||
|
c != null &&
|
||||||
|
(
|
||||||
|
(cu != null && !cu.ReadOnly) || (cg != null && !cg.ReadOnly && cu == null)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) &&
|
||||||
|
(_status == null || st.Status == _status)
|
||||||
|
group st by new
|
||||||
|
{
|
||||||
|
st.Id,
|
||||||
|
st.OrganizationId,
|
||||||
|
st.CipherId,
|
||||||
|
st.Type,
|
||||||
|
st.Status,
|
||||||
|
st.CreationDate,
|
||||||
|
st.RevisionDate
|
||||||
|
} into g
|
||||||
|
select new SecurityTask
|
||||||
|
{
|
||||||
|
Id = g.Key.Id,
|
||||||
|
OrganizationId = g.Key.OrganizationId,
|
||||||
|
CipherId = g.Key.CipherId,
|
||||||
|
Type = g.Key.Type,
|
||||||
|
Status = g.Key.Status,
|
||||||
|
CreationDate = g.Key.CreationDate,
|
||||||
|
RevisionDate = g.Key.RevisionDate
|
||||||
|
};
|
||||||
|
|
||||||
|
return query.OrderByDescending(st => st.CreationDate);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,10 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
using Bit.Core.Vault.Enums;
|
||||||
using Bit.Core.Vault.Repositories;
|
using Bit.Core.Vault.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.Vault.Models;
|
using Bit.Infrastructure.EntityFramework.Vault.Models;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Vault.Repositories.Queries;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Bit.Infrastructure.EntityFramework.Vault.Repositories;
|
namespace Bit.Infrastructure.EntityFramework.Vault.Repositories;
|
||||||
@ -11,4 +14,15 @@ public class SecurityTaskRepository : Repository<Core.Vault.Entities.SecurityTas
|
|||||||
public SecurityTaskRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
|
public SecurityTaskRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
|
||||||
: base(serviceScopeFactory, mapper, (context) => context.SecurityTasks)
|
: base(serviceScopeFactory, mapper, (context) => context.SecurityTasks)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<ICollection<Core.Vault.Entities.SecurityTask>> GetManyByUserIdStatusAsync(Guid userId,
|
||||||
|
SecurityTaskStatus? status = null)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var query = new SecurityTaskReadByUserIdStatusQuery(userId, status);
|
||||||
|
var data = await query.Run(dbContext).ToListAsync();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[SecurityTask_ReadByUserIdStatus]
|
||||||
|
@UserId UNIQUEIDENTIFIER,
|
||||||
|
@Status TINYINT = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
ST.Id,
|
||||||
|
ST.OrganizationId,
|
||||||
|
ST.CipherId,
|
||||||
|
ST.Type,
|
||||||
|
ST.Status,
|
||||||
|
ST.CreationDate,
|
||||||
|
ST.RevisionDate
|
||||||
|
FROM
|
||||||
|
[dbo].[SecurityTaskView] ST
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[OrganizationUserView] OU ON OU.[OrganizationId] = ST.[OrganizationId]
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[Organization] O ON O.[Id] = ST.[OrganizationId]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[CipherView] C ON C.[Id] = ST.[CipherId]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[CollectionCipher] CC ON CC.[CipherId] = C.[Id] AND C.[Id] IS NOT NULL
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[CollectionUser] CU ON CU.[CollectionId] = CC.[CollectionId] AND CU.[OrganizationUserId] = OU.[Id] AND C.[Id] IS NOT NULL
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[GroupUser] GU ON GU.[OrganizationUserId] = OU.[Id] AND CU.[CollectionId] IS NULL AND C.[Id] IS NOT NULL
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[CollectionGroup] CG ON CG.[GroupId] = GU.[GroupId] AND CG.[CollectionId] = CC.[CollectionId]
|
||||||
|
WHERE
|
||||||
|
OU.[UserId] = @UserId
|
||||||
|
AND OU.[Status] = 2 -- Ensure user is confirmed
|
||||||
|
AND O.[Enabled] = 1
|
||||||
|
AND (
|
||||||
|
ST.[CipherId] IS NULL
|
||||||
|
OR (
|
||||||
|
C.[Id] IS NOT NULL
|
||||||
|
AND (
|
||||||
|
CU.[ReadOnly] = 0
|
||||||
|
OR CG.[ReadOnly] = 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AND ST.[Status] = COALESCE(@Status, ST.[Status])
|
||||||
|
GROUP BY
|
||||||
|
ST.Id,
|
||||||
|
ST.OrganizationId,
|
||||||
|
ST.CipherId,
|
||||||
|
ST.Type,
|
||||||
|
ST.Status,
|
||||||
|
ST.CreationDate,
|
||||||
|
ST.RevisionDate
|
||||||
|
ORDER BY ST.[CreationDate] DESC
|
||||||
|
END
|
@ -0,0 +1,22 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Bit.Core.Vault.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.IntegrationTest.Comparers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines the equality of two SecurityTask objects.
|
||||||
|
/// </summary>
|
||||||
|
public class SecurityTaskComparer : IEqualityComparer<SecurityTask>
|
||||||
|
{
|
||||||
|
public bool Equals(SecurityTask x, SecurityTask y)
|
||||||
|
{
|
||||||
|
return x.Id.Equals(y.Id) &&
|
||||||
|
x.Type.Equals(y.Type) &&
|
||||||
|
x.Status.Equals(y.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode([DisallowNull] SecurityTask obj)
|
||||||
|
{
|
||||||
|
return base.GetHashCode();
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,13 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Bit.Core.Vault.Enums;
|
using Bit.Core.Vault.Enums;
|
||||||
using Bit.Core.Vault.Repositories;
|
using Bit.Core.Vault.Repositories;
|
||||||
|
using Bit.Infrastructure.IntegrationTest.Comparers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Infrastructure.IntegrationTest.Vault.Repositories;
|
namespace Bit.Infrastructure.IntegrationTest.Vault.Repositories;
|
||||||
@ -120,4 +124,103 @@ public class SecurityTaskRepositoryTests
|
|||||||
Assert.Equal(task.Id, updatedTask.Id);
|
Assert.Equal(task.Id, updatedTask.Id);
|
||||||
Assert.Equal(SecurityTaskStatus.Completed, updatedTask.Status);
|
Assert.Equal(SecurityTaskStatus.Completed, updatedTask.Status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task GetManyByUserIdAsync_ReturnsExpectedTasks(
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
ICipherRepository cipherRepository,
|
||||||
|
ISecurityTaskRepository securityTaskRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
ICollectionRepository collectionRepository)
|
||||||
|
{
|
||||||
|
var user = await userRepository.CreateAsync(new User
|
||||||
|
{
|
||||||
|
Name = "Test User",
|
||||||
|
Email = $"test+{Guid.NewGuid()}@email.com",
|
||||||
|
ApiKey = "TEST",
|
||||||
|
SecurityStamp = "stamp",
|
||||||
|
});
|
||||||
|
|
||||||
|
var organization = await organizationRepository.CreateAsync(new Organization
|
||||||
|
{
|
||||||
|
Name = "Test Org",
|
||||||
|
PlanType = PlanType.EnterpriseAnnually,
|
||||||
|
Plan = "Test Plan",
|
||||||
|
BillingEmail = "billing@email.com"
|
||||||
|
});
|
||||||
|
|
||||||
|
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
UserId = user.Id,
|
||||||
|
Status = OrganizationUserStatusType.Confirmed
|
||||||
|
});
|
||||||
|
|
||||||
|
var collection = await collectionRepository.CreateAsync(new Collection
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
Name = "Test Collection 1",
|
||||||
|
});
|
||||||
|
|
||||||
|
var collection2 = await collectionRepository.CreateAsync(new Collection
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
Name = "Test Collection 2",
|
||||||
|
});
|
||||||
|
|
||||||
|
var cipher1 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", };
|
||||||
|
await cipherRepository.CreateAsync(cipher1, [collection.Id, collection2.Id]);
|
||||||
|
|
||||||
|
var cipher2 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", };
|
||||||
|
await cipherRepository.CreateAsync(cipher2, [collection.Id]);
|
||||||
|
|
||||||
|
var task1 = await securityTaskRepository.CreateAsync(new SecurityTask
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
CipherId = cipher1.Id,
|
||||||
|
Status = SecurityTaskStatus.Pending,
|
||||||
|
Type = SecurityTaskType.UpdateAtRiskCredential,
|
||||||
|
});
|
||||||
|
|
||||||
|
var task2 = await securityTaskRepository.CreateAsync(new SecurityTask
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
CipherId = cipher2.Id,
|
||||||
|
Status = SecurityTaskStatus.Completed,
|
||||||
|
Type = SecurityTaskType.UpdateAtRiskCredential,
|
||||||
|
});
|
||||||
|
|
||||||
|
var task3 = await securityTaskRepository.CreateAsync(new SecurityTask
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
CipherId = cipher2.Id,
|
||||||
|
Status = SecurityTaskStatus.Pending,
|
||||||
|
Type = SecurityTaskType.UpdateAtRiskCredential,
|
||||||
|
});
|
||||||
|
|
||||||
|
await collectionRepository.UpdateUsersAsync(collection.Id,
|
||||||
|
new List<CollectionAccessSelection>
|
||||||
|
{
|
||||||
|
new() {Id = orgUser.Id, ReadOnly = false, HidePasswords = false, Manage = true}
|
||||||
|
});
|
||||||
|
|
||||||
|
var allTasks = await securityTaskRepository.GetManyByUserIdStatusAsync(user.Id);
|
||||||
|
Assert.Equal(3, allTasks.Count);
|
||||||
|
Assert.Contains(task1, allTasks, new SecurityTaskComparer());
|
||||||
|
Assert.Contains(task2, allTasks, new SecurityTaskComparer());
|
||||||
|
Assert.Contains(task3, allTasks, new SecurityTaskComparer());
|
||||||
|
|
||||||
|
var pendingTasks = await securityTaskRepository.GetManyByUserIdStatusAsync(user.Id, SecurityTaskStatus.Pending);
|
||||||
|
Assert.Equal(2, pendingTasks.Count);
|
||||||
|
Assert.Contains(task1, pendingTasks, new SecurityTaskComparer());
|
||||||
|
Assert.Contains(task3, pendingTasks, new SecurityTaskComparer());
|
||||||
|
Assert.DoesNotContain(task2, pendingTasks, new SecurityTaskComparer());
|
||||||
|
|
||||||
|
var completedTasks = await securityTaskRepository.GetManyByUserIdStatusAsync(user.Id, SecurityTaskStatus.Completed);
|
||||||
|
Assert.Single(completedTasks);
|
||||||
|
Assert.Contains(task2, completedTasks, new SecurityTaskComparer());
|
||||||
|
Assert.DoesNotContain(task1, completedTasks, new SecurityTaskComparer());
|
||||||
|
Assert.DoesNotContain(task3, completedTasks, new SecurityTaskComparer());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
-- Security Task Read By UserId Status
|
||||||
|
-- Stored Procedure: ReadByUserIdStatus
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[SecurityTask_ReadByUserIdStatus]
|
||||||
|
@UserId UNIQUEIDENTIFIER,
|
||||||
|
@Status TINYINT = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
ST.Id,
|
||||||
|
ST.OrganizationId,
|
||||||
|
ST.CipherId,
|
||||||
|
ST.Type,
|
||||||
|
ST.Status,
|
||||||
|
ST.CreationDate,
|
||||||
|
ST.RevisionDate
|
||||||
|
FROM
|
||||||
|
[dbo].[SecurityTaskView] ST
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[OrganizationUserView] OU ON OU.[OrganizationId] = ST.[OrganizationId]
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[Organization] O ON O.[Id] = ST.[OrganizationId]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[CipherView] C ON C.[Id] = ST.[CipherId]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[CollectionCipher] CC ON CC.[CipherId] = C.[Id] AND C.[Id] IS NOT NULL
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[CollectionUser] CU ON CU.[CollectionId] = CC.[CollectionId] AND CU.[OrganizationUserId] = OU.[Id] AND C.[Id] IS NOT NULL
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[GroupUser] GU ON GU.[OrganizationUserId] = OU.[Id] AND CU.[CollectionId] IS NULL AND C.[Id] IS NOT NULL
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[CollectionGroup] CG ON CG.[GroupId] = GU.[GroupId] AND CG.[CollectionId] = CC.[CollectionId]
|
||||||
|
WHERE
|
||||||
|
OU.[UserId] = @UserId
|
||||||
|
AND OU.[Status] = 2 -- Ensure user is confirmed
|
||||||
|
AND O.[Enabled] = 1
|
||||||
|
AND (
|
||||||
|
ST.[CipherId] IS NULL
|
||||||
|
OR (
|
||||||
|
C.[Id] IS NOT NULL
|
||||||
|
AND (
|
||||||
|
CU.[ReadOnly] = 0
|
||||||
|
OR CG.[ReadOnly] = 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AND ST.[Status] = COALESCE(@Status, ST.[Status])
|
||||||
|
GROUP BY
|
||||||
|
ST.Id,
|
||||||
|
ST.OrganizationId,
|
||||||
|
ST.CipherId,
|
||||||
|
ST.Type,
|
||||||
|
ST.Status,
|
||||||
|
ST.CreationDate,
|
||||||
|
ST.RevisionDate
|
||||||
|
ORDER BY ST.[CreationDate] DESC
|
||||||
|
END
|
||||||
|
GO
|
Loading…
x
Reference in New Issue
Block a user