Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ public UserCipherDetailsQuery(Guid? userId)

public virtual IQueryable<CipherDetails> Run(DatabaseContext dbContext)
{
if (!_userId.HasValue)
{
throw new InvalidOperationException("UserCipherDetailsQuery requires a non-null userId.");
}

var userId = _userId.Value;
var query = from c in dbContext.Ciphers

join ou in dbContext.OrganizationUsers
Expand Down Expand Up @@ -49,9 +55,11 @@ from g in g_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 (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

select new
{
Expand All @@ -72,7 +80,6 @@ from cg in cg_g.DefaultIfEmpty()
OrganizationUseTotp = o.UseTotp,
c.Reprompt,
c.Key,
c.ArchivedDate
};

var query2 = from c in dbContext.Ciphers
Expand All @@ -96,7 +103,6 @@ from cg in cg_g.DefaultIfEmpty()
OrganizationUseTotp = false,
c.Reprompt,
c.Key,
c.ArchivedDate
};

var union = query.Union(query2).Select(c => new CipherDetails
Expand All @@ -118,7 +124,10 @@ from cg in cg_g.DefaultIfEmpty()
Manage = c.Manage,
OrganizationUseTotp = c.OrganizationUseTotp,
Key = c.Key,
ArchivedDate = c.ArchivedDate
ArchivedDate = dbContext.CipherArchives
.Where(ca => ca.CipherId == c.Id && ca.UserId == userId)
.Select(ca => (DateTime?)ca.ArchivedDate)
.FirstOrDefault(),
});
return union;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -794,21 +794,66 @@
{
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)
.ToListAsync())
.Where(ucd => ids.Contains(ucd.Id) && ucd.Edit)
.ToList();

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;
});

Expand All @@ -819,6 +864,7 @@
}
}


private async Task<DateTime> ToggleDeleteCipherStatesAsync(IEnumerable<Guid> ids, Guid userId, CipherStateAction action)
{
static bool FilterDeletedDate(CipherStateAction action, CipherDetails ucd)
Expand Down Expand Up @@ -974,7 +1020,7 @@
var foldersJson = NSL.JObject.Parse(cipher.Folders);
if (foldersJson == null && folderId.HasValue)
{
foldersJson.Add(userId.ToString(), folderId.Value);

Check warning on line 1023 in src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs

View workflow job for this annotation

GitHub Actions / Sonar / Quality scan

'foldersJson' is null on at least one execution path. (https://rules.sonarsource.com/csharp/RSPEC-2259)
}
else if (foldersJson != null && folderId.HasValue)
{
Expand All @@ -982,7 +1028,7 @@
}
else
{
foldersJson.Remove(userId.ToString());

Check warning on line 1031 in src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs

View workflow job for this annotation

GitHub Actions / Sonar / Quality scan

'foldersJson' is null on at least one execution path. (https://rules.sonarsource.com/csharp/RSPEC-2259)
}

var favoritesJson = NSL.JObject.Parse(cipher.Favorites);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 can't 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;
}
}
5 changes: 4 additions & 1 deletion src/Sql/dbo/Vault/Functions/CipherDetails.sql
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ SELECT
C.[DeletedDate],
C.[Reprompt],
C.[Key],
C.[ArchivedDate]
CA.[ArchivedDate]
FROM
[dbo].[Cipher] C
LEFT JOIN [dbo].[CipherArchive] CA
ON CA.[CipherId] = C.[Id]
AND CA.[UserId] = @UserId;
44 changes: 37 additions & 7 deletions src/Sql/dbo/Vault/Functions/UserCipherDetails.sql
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,29 @@ WITH [CTE] AS (
AND [Status] = 2 -- Confirmed
)
SELECT
C.*,
C.Id,
C.UserId,
C.OrganizationId,
C.Type,
C.Data,
C.Attachments,
C.CreationDate,
C.RevisionDate,
C.Favorite,
C.FolderId,
C.DeletedDate,
CA.ArchivedDate,
C.Reprompt,
C.[Key],
CASE
WHEN COALESCE(CU.[ReadOnly], CG.[ReadOnly], 0) = 0
THEN 1
ELSE 0
END [Edit],
CASE
WHEN COALESCE(CU.[HidePasswords], CG.[HidePasswords], 0) = 0
THEN 1
ELSE 0
WHEN COALESCE(CU.[HidePasswords], CG.[HidePasswords], 0) = 0
THEN 1
ELSE 0
END [ViewPassword],
CASE
WHEN COALESCE(CU.[Manage], CG.[Manage], 0) = 1
Expand Down Expand Up @@ -49,19 +62,36 @@ LEFT JOIN
[dbo].[Group] G ON G.[Id] = GU.[GroupId]
LEFT JOIN
[dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId]
LEFT JOIN
[dbo].[CipherArchive] CA ON CA.[CipherId] = C.[Id] AND CA.[UserId] = @UserId
WHERE
CU.[CollectionId] IS NOT NULL
OR CG.[CollectionId] IS NOT NULL

UNION ALL

SELECT
*,
C.Id,
C.UserId,
C.OrganizationId,
C.Type,
C.Data,
C.Attachments,
C.CreationDate,
C.RevisionDate,
C.Favorite,
C.FolderId,
C.DeletedDate,
CA.ArchivedDate,
C.Reprompt,
C.[Key],
1 [Edit],
1 [ViewPassword],
1 [Manage],
0 [OrganizationUseTotp]
FROM
[dbo].[CipherDetails](@UserId)
[dbo].[CipherDetails](@UserId) AS C
LEFT JOIN
[dbo].[CipherArchive] CA ON CA.[CipherId] = C.[Id] AND CA.[UserId] = @UserId
WHERE
[UserId] = @UserId
C.[UserId] = @UserId;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐ŸŽจ I think it would be preferable to put the additional join in the CipherDetails.sql function that way we don't have to repeat it in all these CipherDetails_* sprocs (or anywhere else the function is used).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shane-melton Totally agree. I had done that originally but then had a bunch of failing sql and tests that couldn't seem to handle the nested join. I'll see if I can get them to work again.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shane-melton this is now resolved in da0131c

Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ SELECT
[Key],
[OrganizationUseTotp],
[ArchivedDate],
MAX ([Edit]) AS [Edit],
MAX ([ViewPassword]) AS [ViewPassword],
MAX ([Manage]) AS [Manage]
MAX ([Edit]) AS Edit,
MAX ([ViewPassword]) AS ViewPassword,
MAX ([Manage]) AS Manage
FROM
[dbo].[UserCipherDetails](@UserId)
WHERE
Expand Down
46 changes: 19 additions & 27 deletions src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Archive.sql
Original file line number Diff line number Diff line change
@@ -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
DECLARE @UtcNow DATETIME2(7) = SYSUTCDATETIME();

WITH CipherIdsToArchive AS
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[UserId] UNIQUEIDENTIFIER NULL
SELECT DISTINCT C.Id
FROM [dbo].[Cipher] C
INNER JOIN @Ids I ON C.Id = I.[Id]
WHERE (C.[UserId] = @UserId)
)

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();
UPDATE
[dbo].[Cipher]
SET
[ArchivedDate] = @UtcNow,
[RevisionDate] = @UtcNow
WHERE
[Id] IN (SELECT [Id] FROM #Temp)
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
Loading
Loading