1
0
mirror of https://github.com/bitwarden/server synced 2025-12-15 15:53:59 +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)
{
var userId = _userId;
var query = from c in dbContext.Ciphers
join ou in dbContext.OrganizationUsers
@@ -49,7 +50,15 @@ public class UserCipherDetailsQuery : IQuery<CipherDetails>
join cg in dbContext.CollectionGroups
on new { cc.CollectionId, gu.GroupId } equals
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 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
@@ -72,11 +81,19 @@ public class UserCipherDetailsQuery : IQuery<CipherDetails>
OrganizationUseTotp = o.UseTotp,
c.Reprompt,
c.Key,
c.ArchivedDate
ArchivedDate = (DateTime?)ca.ArchivedDate
};
var query2 = from c in dbContext.Ciphers
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
{
c.Id,
@@ -96,7 +113,7 @@ public class UserCipherDetailsQuery : IQuery<CipherDetails>
OrganizationUseTotp = false,
c.Reprompt,
c.Key,
c.ArchivedDate
ArchivedDate = (DateTime?)ca.ArchivedDate
};
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.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 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()
join c in cipherEntitiesToCheck
on ucd.Id equals c.Id
where ucd.Edit && FilterArchivedDate(action, ucd)
select c;
var userCipherDetails = await userCipherDetailsQuery
.Run(dbContext)
.Where(ucd => ids.Contains(ucd.Id) && ucd.Edit)
.ToListAsync();
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 =>
{
dbContext.Attach(cipher);
cipher.ArchivedDate = action == CipherStateAction.Unarchive ? null : 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)
{
static bool FilterDeletedDate(CipherStateAction action, CipherDetails ucd)

View File

@@ -9,33 +9,72 @@ public class CipherDetailsQuery : IQuery<CipherDetails>
{
private readonly Guid? _userId;
private readonly bool _ignoreFolders;
public CipherDetailsQuery(Guid? userId, bool ignoreFolders = false)
{
_userId = userId;
_ignoreFolders = ignoreFolders;
}
public virtual IQueryable<CipherDetails> Run(DatabaseContext dbContext)
{
var query = from c in dbContext.Ciphers
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 = _userId.HasValue && c.Favorites != null && c.Favorites.ToLowerInvariant().Contains($"\"{_userId}\":true"),
FolderId = (_ignoreFolders || !_userId.HasValue || c.Folders == null || !c.Folders.ToLowerInvariant().Contains(_userId.Value.ToString())) ?
null :
CoreHelpers.LoadClassFromJsonData<Dictionary<Guid, Guid>>(c.Folders)[_userId.Value],
ArchivedDate = c.ArchivedDate,
};
return query;
// No user context: we cant resolve per-user favorites/folders/archive.
if (!_userId.HasValue)
{
var query = from c in dbContext.Ciphers
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 = false,
FolderId = null,
ArchivedDate = null,
};
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
[dbo].[UserCipherDetails](@UserId)
LEFT JOIN [dbo].[CipherArchive] ca
ON ca.CipherId = c.Id
AND ca.UserId = @UserId
WHERE
END

View File

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

View File

@@ -1,39 +1,31 @@
CREATE PROCEDURE [dbo].[Cipher_Archive]
@Ids AS [dbo].[GuidIdArray] READONLY,
@UserId AS UNIQUEIDENTIFIER
@Ids dbo.GuidIdArray READONLY,
@UserId UNIQUEIDENTIFIER
AS
BEGIN
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)
SET NOCOUNT ON;
DECLARE @UtcNow DATETIME2(7) = SYSUTCDATETIME();
UPDATE
[dbo].[Cipher]
SET
[ArchivedDate] = @UtcNow,
[RevisionDate] = @UtcNow
WHERE
[Id] IN (SELECT [Id] FROM #Temp)
WITH CipherIdsToArchive AS
(
SELECT DISTINCT C.Id
FROM [dbo].[Cipher] C
INNER JOIN @Ids I ON C.Id = I.[Id]
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
DROP TABLE #Temp
SELECT @UtcNow
END

View File

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

View File

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

View File

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