1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-16 07:07:32 -05:00

[PM-14406] Security Task Notifications (#5344)

* initial commit of `CipherOrganizationPermission_GetManyByUserId`

* create queries to get all of the security tasks that are actionable by a user

- A task is "actionable" when the user has manage permissions for that cipher

* rename query

* return the user's email from the query as well

* Add email notification for at-risk passwords

- Added email layouts for security tasks

* add push notification for security tasks

* update entity framework to match stored procedure plus testing

* update date of migration and remove orderby

* add push service to security task controller

* rename `SyncSecurityTasksCreated` to `SyncNotification`

* remove duplicate return

* remove unused directive

* remove unneeded new notification type

* use `createNotificationCommand` to alert all platforms

* return the cipher id that is associated with the security task and store the security task id on the notification entry

* Add `TaskId` to the output model of `GetUserSecurityTasksByCipherIdsAsync`

* move notification logic to command

* use TaskId from `_getSecurityTasksNotificationDetailsQuery`

* add service

* only push last notification for each user

* formatting

* refactor `CreateNotificationCommand` parameter to `sendPush`

* flip boolean in test

* update interface to match usage

* do not push any of the security related notifications to the user

* add `PendingSecurityTasks` push type

* add push notification for pending security tasks
This commit is contained in:
Nick Krantz
2025-02-27 08:34:42 -06:00
committed by GitHub
parent a2e665cb96
commit 1267332b5b
35 changed files with 893 additions and 8 deletions

View File

@ -348,6 +348,51 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
}
}
public async Task<ICollection<UserSecurityTaskCipher>> GetUserSecurityTasksByCipherIdsAsync(Guid organizationId, IEnumerable<Core.Vault.Entities.SecurityTask> tasks)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var cipherIds = tasks.Where(t => t.CipherId.HasValue).Select(t => t.CipherId.Value);
var dbContext = GetDatabaseContext(scope);
var query = new UserSecurityTasksByCipherIdsQuery(organizationId, cipherIds).Run(dbContext);
ICollection<UserSecurityTaskCipher> userTaskCiphers;
// SQLite does not support the GROUP BY clause
if (dbContext.Database.IsSqlite())
{
userTaskCiphers = (await query.ToListAsync())
.GroupBy(c => new { c.UserId, c.Email, c.CipherId })
.Select(g => new UserSecurityTaskCipher
{
UserId = g.Key.UserId,
Email = g.Key.Email,
CipherId = g.Key.CipherId,
}).ToList();
}
else
{
var groupByQuery = from p in query
group p by new { p.UserId, p.Email, p.CipherId }
into g
select new UserSecurityTaskCipher
{
UserId = g.Key.UserId,
CipherId = g.Key.CipherId,
Email = g.Key.Email,
};
userTaskCiphers = await groupByQuery.ToListAsync();
}
foreach (var userTaskCipher in userTaskCiphers)
{
userTaskCipher.TaskId = tasks.First(t => t.CipherId == userTaskCipher.CipherId).Id;
}
return userTaskCiphers;
}
}
public async Task<CipherDetails> GetByIdAsync(Guid id, Guid userId)
{
using (var scope = ServiceScopeFactory.CreateScope())

View File

@ -0,0 +1,71 @@
using Bit.Core.Vault.Models.Data;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories.Queries;
namespace Bit.Infrastructure.EntityFramework.Vault.Repositories.Queries;
public class UserSecurityTasksByCipherIdsQuery : IQuery<UserCipherForTask>
{
private readonly Guid _organizationId;
private readonly IEnumerable<Guid> _cipherIds;
public UserSecurityTasksByCipherIdsQuery(Guid organizationId, IEnumerable<Guid> cipherIds)
{
_organizationId = organizationId;
_cipherIds = cipherIds;
}
public IQueryable<UserCipherForTask> Run(DatabaseContext dbContext)
{
var baseCiphers =
from c in dbContext.Ciphers
where _cipherIds.Contains(c.Id)
join o in dbContext.Organizations
on c.OrganizationId equals o.Id
where o.Id == _organizationId && o.Enabled
select c;
var userPermissions =
from c in baseCiphers
join cc in dbContext.CollectionCiphers
on c.Id equals cc.CipherId
join cu in dbContext.CollectionUsers
on cc.CollectionId equals cu.CollectionId
join ou in dbContext.OrganizationUsers
on cu.OrganizationUserId equals ou.Id
where ou.OrganizationId == _organizationId
&& cu.Manage == true
select new { ou.UserId, c.Id };
var groupPermissions =
from c in baseCiphers
join cc in dbContext.CollectionCiphers
on c.Id equals cc.CipherId
join cg in dbContext.CollectionGroups
on cc.CollectionId equals cg.CollectionId
join gu in dbContext.GroupUsers
on cg.GroupId equals gu.GroupId
join ou in dbContext.OrganizationUsers
on gu.OrganizationUserId equals ou.Id
where ou.OrganizationId == _organizationId
&& cg.Manage == true
&& !userPermissions.Any(up => up.Id == c.Id && up.UserId == ou.UserId)
select new { ou.UserId, c.Id };
return userPermissions.Union(groupPermissions)
.Join(
dbContext.Users,
p => p.UserId,
u => u.Id,
(p, u) => new { p.UserId, p.Id, u.Email }
)
.GroupBy(x => new { x.UserId, x.Email, x.Id })
.Select(g => new UserCipherForTask
{
UserId = (Guid)g.Key.UserId,
Email = g.Key.Email,
CipherId = g.Key.Id
})
.OrderByDescending(x => x.Email);
}
}