1
0
mirror of https://github.com/bitwarden/server synced 2025-12-16 16:23:31 +00:00

update to cipher archive

This commit is contained in:
jaasen-livefront
2025-11-14 18:48:22 -08:00
parent bde0ab4a63
commit 5b300de5f4
9 changed files with 179 additions and 97 deletions

View File

@@ -18,6 +18,7 @@ public class UserCipherDetailsQuery : IQuery<CipherDetails>
public virtual IQueryable<CipherDetails> Run(DatabaseContext dbContext) public virtual IQueryable<CipherDetails> Run(DatabaseContext dbContext)
{ {
var userId = _userId;
var query = from c in dbContext.Ciphers var query = from c in dbContext.Ciphers
join ou in dbContext.OrganizationUsers join ou in dbContext.OrganizationUsers
@@ -49,7 +50,15 @@ public class UserCipherDetailsQuery : IQuery<CipherDetails>
join cg in dbContext.CollectionGroups join cg in dbContext.CollectionGroups
on new { cc.CollectionId, gu.GroupId } equals on new { cc.CollectionId, gu.GroupId } equals
new { cg.CollectionId, cg.GroupId } into cg_g new { cg.CollectionId, cg.GroupId } into cg_g
join ca in dbContext.CipherArchives
on c.Id equals ca.CipherId
into caGroup
from cg in cg_g.DefaultIfEmpty() from cg in cg_g.DefaultIfEmpty()
from ca in caGroup
.Where(a => userId.HasValue && a.UserId == userId.Value)
.DefaultIfEmpty()
where (cu == null ? (Guid?)null : cu.CollectionId) != null || (cg == null ? (Guid?)null : cg.CollectionId) != null where (cu == null ? (Guid?)null : cu.CollectionId) != null || (cg == null ? (Guid?)null : cg.CollectionId) != null
@@ -72,11 +81,19 @@ public class UserCipherDetailsQuery : IQuery<CipherDetails>
OrganizationUseTotp = o.UseTotp, OrganizationUseTotp = o.UseTotp,
c.Reprompt, c.Reprompt,
c.Key, c.Key,
c.ArchivedDate ArchivedDate = (DateTime?)ca.ArchivedDate
}; };
var query2 = from c in dbContext.Ciphers var query2 = from c in dbContext.Ciphers
where c.UserId == _userId where c.UserId == _userId
join ca in dbContext.CipherArchives
on c.Id equals ca.CipherId
into caGroup
from ca in caGroup
.Where(a => userId.HasValue && a.UserId == userId.Value)
.DefaultIfEmpty()
select new select new
{ {
c.Id, c.Id,
@@ -96,7 +113,7 @@ public class UserCipherDetailsQuery : IQuery<CipherDetails>
OrganizationUseTotp = false, OrganizationUseTotp = false,
c.Reprompt, c.Reprompt,
c.Key, c.Key,
c.ArchivedDate ArchivedDate = (DateTime?)ca.ArchivedDate
}; };
var union = query.Union(query2).Select(c => new CipherDetails var union = query.Union(query2).Select(c => new CipherDetails

View File

@@ -786,7 +786,7 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
{ {
CipherStateAction.Unarchive => ucd.ArchivedDate != null, CipherStateAction.Unarchive => ucd.ArchivedDate != null,
CipherStateAction.Archive => ucd.ArchivedDate == null, CipherStateAction.Archive => ucd.ArchivedDate == null,
_ => true _ => true,
}; };
} }
@@ -794,21 +794,65 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
{ {
var dbContext = GetDatabaseContext(scope); var dbContext = GetDatabaseContext(scope);
var userCipherDetailsQuery = new UserCipherDetailsQuery(userId); var userCipherDetailsQuery = new UserCipherDetailsQuery(userId);
var cipherEntitiesToCheck = await dbContext.Ciphers.Where(c => ids.Contains(c.Id)).ToListAsync();
var query = from ucd in await userCipherDetailsQuery.Run(dbContext).ToListAsync() var userCipherDetails = await userCipherDetailsQuery
join c in cipherEntitiesToCheck .Run(dbContext)
on ucd.Id equals c.Id .Where(ucd => ids.Contains(ucd.Id) && ucd.Edit)
where ucd.Edit && FilterArchivedDate(action, ucd) .ToListAsync();
select c;
var utcNow = DateTime.UtcNow; var utcNow = DateTime.UtcNow;
var cipherIdsToModify = query.Select(c => c.Id);
var cipherEntitiesToModify = dbContext.Ciphers.Where(x => cipherIdsToModify.Contains(x.Id));
var cipherIdsToModify = userCipherDetails
.Where(ucd => FilterArchivedDate(action, ucd))
.Select(ucd => ucd.Id)
.Distinct()
.ToList();
if (!cipherIdsToModify.Any())
{
return utcNow;
}
if (action == CipherStateAction.Archive)
{
var existingArchiveCipherIds = await dbContext.CipherArchives
.Where(ca => ca.UserId == userId && cipherIdsToModify.Contains(ca.CipherId))
.Select(ca => ca.CipherId)
.ToListAsync();
var cipherIdsToArchive = cipherIdsToModify
.Except(existingArchiveCipherIds)
.ToList();
if (cipherIdsToArchive.Any())
{
var archives = cipherIdsToArchive.Select(id => new CipherArchive
{
CipherId = id,
UserId = userId,
ArchivedDate = utcNow,
});
await dbContext.CipherArchives.AddRangeAsync(archives);
}
}
else if (action == CipherStateAction.Unarchive)
{
var archivesToRemove = await dbContext.CipherArchives
.Where(ca => ca.UserId == userId && cipherIdsToModify.Contains(ca.CipherId))
.ToListAsync();
if (archivesToRemove.Count > 0)
{
dbContext.CipherArchives.RemoveRange(archivesToRemove);
}
}
// Keep the behavior that archive/unarchive "touches" the cipher row for sync.
var cipherEntitiesToModify = dbContext.Ciphers.Where(c => cipherIdsToModify.Contains(c.Id));
await cipherEntitiesToModify.ForEachAsync(cipher => await cipherEntitiesToModify.ForEachAsync(cipher =>
{ {
dbContext.Attach(cipher); dbContext.Attach(cipher);
cipher.ArchivedDate = action == CipherStateAction.Unarchive ? null : utcNow;
cipher.RevisionDate = utcNow; cipher.RevisionDate = utcNow;
}); });
@@ -819,6 +863,7 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
} }
} }
private async Task<DateTime> ToggleDeleteCipherStatesAsync(IEnumerable<Guid> ids, Guid userId, CipherStateAction action) private async Task<DateTime> ToggleDeleteCipherStatesAsync(IEnumerable<Guid> ids, Guid userId, CipherStateAction action)
{ {
static bool FilterDeletedDate(CipherStateAction action, CipherDetails ucd) static bool FilterDeletedDate(CipherStateAction action, CipherDetails ucd)

View File

@@ -9,33 +9,72 @@ public class CipherDetailsQuery : IQuery<CipherDetails>
{ {
private readonly Guid? _userId; private readonly Guid? _userId;
private readonly bool _ignoreFolders; private readonly bool _ignoreFolders;
public CipherDetailsQuery(Guid? userId, bool ignoreFolders = false) public CipherDetailsQuery(Guid? userId, bool ignoreFolders = false)
{ {
_userId = userId; _userId = userId;
_ignoreFolders = ignoreFolders; _ignoreFolders = ignoreFolders;
} }
public virtual IQueryable<CipherDetails> Run(DatabaseContext dbContext) public virtual IQueryable<CipherDetails> Run(DatabaseContext dbContext)
{ {
var query = from c in dbContext.Ciphers // No user context: we cant resolve per-user favorites/folders/archive.
select new CipherDetails if (!_userId.HasValue)
{ {
Id = c.Id, var query = from c in dbContext.Ciphers
UserId = c.UserId, select new CipherDetails
OrganizationId = c.OrganizationId, {
Type = c.Type, Id = c.Id,
Data = c.Data, UserId = c.UserId,
Attachments = c.Attachments, OrganizationId = c.OrganizationId,
CreationDate = c.CreationDate, Type = c.Type,
RevisionDate = c.RevisionDate, Data = c.Data,
DeletedDate = c.DeletedDate, Attachments = c.Attachments,
Reprompt = c.Reprompt, CreationDate = c.CreationDate,
Key = c.Key, RevisionDate = c.RevisionDate,
Favorite = _userId.HasValue && c.Favorites != null && c.Favorites.ToLowerInvariant().Contains($"\"{_userId}\":true"), DeletedDate = c.DeletedDate,
FolderId = (_ignoreFolders || !_userId.HasValue || c.Folders == null || !c.Folders.ToLowerInvariant().Contains(_userId.Value.ToString())) ? Reprompt = c.Reprompt,
null : Key = c.Key,
CoreHelpers.LoadClassFromJsonData<Dictionary<Guid, Guid>>(c.Folders)[_userId.Value], Favorite = false,
ArchivedDate = c.ArchivedDate, FolderId = null,
}; ArchivedDate = null,
return query; };
return query;
}
var userId = _userId.Value;
var queryWithArchive =
from c in dbContext.Ciphers
join ca in dbContext.CipherArchives
on new { CipherId = c.Id, UserId = userId }
equals new { CipherId = ca.CipherId, ca.UserId }
into caGroup
from ca in caGroup.DefaultIfEmpty()
select new CipherDetails
{
Id = c.Id,
UserId = c.UserId,
OrganizationId = c.OrganizationId,
Type = c.Type,
Data = c.Data,
Attachments = c.Attachments,
CreationDate = c.CreationDate,
RevisionDate = c.RevisionDate,
DeletedDate = c.DeletedDate,
Reprompt = c.Reprompt,
Key = c.Key,
Favorite = c.Favorites != null &&
c.Favorites.ToLowerInvariant().Contains($"\"{userId}\":true"),
FolderId = (_ignoreFolders ||
c.Folders == null ||
!c.Folders.ToLowerInvariant().Contains(userId.ToString()))
? null
: CoreHelpers.LoadClassFromJsonData<Dictionary<Guid, Guid>>(c.Folders)[userId],
ArchivedDate = ca.ArchivedDate,
};
return queryWithArchive;
} }
} }

View File

@@ -8,4 +8,8 @@ BEGIN
* *
FROM FROM
[dbo].[UserCipherDetails](@UserId) [dbo].[UserCipherDetails](@UserId)
LEFT JOIN [dbo].[CipherArchive] ca
ON ca.CipherId = c.Id
AND ca.UserId = @UserId
WHERE
END END

View File

@@ -12,6 +12,9 @@ BEGIN
0 [OrganizationUseTotp] 0 [OrganizationUseTotp]
FROM FROM
[dbo].[CipherDetails](@UserId) [dbo].[CipherDetails](@UserId)
LEFT JOIN [dbo].[CipherArchive] ca
ON ca.CipherId = c.Id
AND ca.UserId = @UserId
WHERE WHERE
[UserId] = @UserId [UserId] = @UserId
END END

View File

@@ -1,39 +1,31 @@
CREATE PROCEDURE [dbo].[Cipher_Archive] CREATE PROCEDURE [dbo].[Cipher_Archive]
@Ids AS [dbo].[GuidIdArray] READONLY, @Ids dbo.GuidIdArray READONLY,
@UserId AS UNIQUEIDENTIFIER @UserId UNIQUEIDENTIFIER
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON;
CREATE TABLE #Temp
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[UserId] UNIQUEIDENTIFIER NULL
)
INSERT INTO #Temp
SELECT
[Id],
[UserId]
FROM
[dbo].[UserCipherDetails](@UserId)
WHERE
[Edit] = 1
AND [ArchivedDate] IS NULL
AND [Id] IN (SELECT * FROM @Ids)
DECLARE @UtcNow DATETIME2(7) = SYSUTCDATETIME(); DECLARE @UtcNow DATETIME2(7) = SYSUTCDATETIME();
UPDATE
[dbo].[Cipher] WITH CipherIdsToArchive AS
SET (
[ArchivedDate] = @UtcNow, SELECT DISTINCT C.Id
[RevisionDate] = @UtcNow FROM [dbo].[Cipher] C
WHERE INNER JOIN @Ids I ON C.Id = I.[Id]
[Id] IN (SELECT [Id] FROM #Temp) WHERE (C.[UserId] = @UserId)
)
INSERT INTO [dbo].[CipherArchive] (CipherId, UserId, ArchivedDate)
SELECT Cta.Id, @UserId, @UtcNow
FROM CipherIdsToArchive Cta
WHERE NOT EXISTS
(
SELECT 1
FROM [dbo].[CipherArchive] Ca
WHERE Ca.CipherId = Cta.Id
AND Ca.UserId = @UserId
);
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
DROP TABLE #Temp
SELECT @UtcNow SELECT @UtcNow
END END

View File

@@ -1,39 +1,18 @@
CREATE PROCEDURE [dbo].[Cipher_Unarchive] CREATE PROCEDURE [dbo].[Cipher_Unarchive]
@Ids AS [dbo].[GuidIdArray] READONLY, @Ids dbo.GuidIdArray READONLY,
@UserId AS UNIQUEIDENTIFIER @UserId UNIQUEIDENTIFIER
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON;
CREATE TABLE #Temp
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[UserId] UNIQUEIDENTIFIER NULL
)
INSERT INTO #Temp
SELECT
[Id],
[UserId]
FROM
[dbo].[UserCipherDetails](@UserId)
WHERE
[Edit] = 1
AND [ArchivedDate] IS NOT NULL
AND [Id] IN (SELECT * FROM @Ids)
DECLARE @UtcNow DATETIME2(7) = SYSUTCDATETIME(); DECLARE @UtcNow DATETIME2(7) = SYSUTCDATETIME();
UPDATE
[dbo].[Cipher] DELETE Ca
SET FROM [dbo].[CipherArchive] Ca
[ArchivedDate] = NULL, INNER JOIN @Ids I ON Ca.CipherId = I.[Id]
[RevisionDate] = @UtcNow WHERE Ca.UserId = @UserId
WHERE
[Id] IN (SELECT [Id] FROM #Temp)
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
DROP TABLE #Temp SELECT @UtcNow;
END
SELECT @UtcNow
END

View File

@@ -11,8 +11,7 @@
@RevisionDate DATETIME2(7), @RevisionDate DATETIME2(7),
@DeletedDate DATETIME2(7), @DeletedDate DATETIME2(7),
@Reprompt TINYINT, @Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL, @Key VARCHAR(MAX) = NULL
@ArchivedDate DATETIME2(7) = NULL
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON
@@ -31,8 +30,7 @@ BEGIN
[RevisionDate] = @RevisionDate, [RevisionDate] = @RevisionDate,
[DeletedDate] = @DeletedDate, [DeletedDate] = @DeletedDate,
[Reprompt] = @Reprompt, [Reprompt] = @Reprompt,
[Key] = @Key, [Key] = @Key
[ArchivedDate] = @ArchivedDate
WHERE WHERE
[Id] = @Id [Id] = @Id

View File

@@ -22,6 +22,11 @@ ADD CONSTRAINT [FK_CipherArchive_User]
ON DELETE CASCADE; ON DELETE CASCADE;
GO GO
ALTER TABLE [dbo].[CipherArchive]
ADD CONSTRAINT [UX_CipherArchive_CipherId_UserId]
UNIQUE (CipherId, UserId);
GO
CREATE NONCLUSTERED INDEX [IX_CipherArchive_UserId] CREATE NONCLUSTERED INDEX [IX_CipherArchive_UserId]
ON [dbo].[CipherArchive]([UserId]); ON [dbo].[CipherArchive]([UserId]);
GO GO