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:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 can’t 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user