Skip to content

Commit 412c6f9

Browse files
Jingo88withinfocuskejaeger
authored
[PM-11162] Assign to Collection Permission Update (#4844)
Only users with Manage/Edit permissions will be allowed to Assign To Collections. If the user has Can Edit Except Password the collections dropdown will be disabled. --------- Co-authored-by: Matt Bishop <[email protected]> Co-authored-by: kejaeger <[email protected]>
1 parent 90680f4 commit 412c6f9

File tree

6 files changed

+233
-39
lines changed

6 files changed

+233
-39
lines changed

src/Api/Vault/Controllers/CiphersController.cs

+55-2
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,59 @@ private async Task<bool> CanAccessUnassignedCiphersAsync(Guid organizationId)
424424
return false;
425425
}
426426

427+
/// <summary>
428+
/// TODO: Move this to its own authorization handler or equivalent service - AC-2062
429+
/// </summary>
430+
private async Task<bool> CanModifyCipherCollectionsAsync(Guid organizationId, IEnumerable<Guid> cipherIds)
431+
{
432+
// If the user can edit all ciphers for the organization, just check they all belong to the org
433+
if (await CanEditAllCiphersAsync(organizationId))
434+
{
435+
// TODO: This can likely be optimized to only query the requested ciphers and then checking they belong to the org
436+
var orgCiphers = (await _cipherRepository.GetManyByOrganizationIdAsync(organizationId)).ToDictionary(c => c.Id);
437+
438+
// Ensure all requested ciphers are in orgCiphers
439+
if (cipherIds.Any(c => !orgCiphers.ContainsKey(c)))
440+
{
441+
return false;
442+
}
443+
444+
return true;
445+
}
446+
447+
// The user cannot access any ciphers for the organization, we're done
448+
if (!await CanAccessOrganizationCiphersAsync(organizationId))
449+
{
450+
return false;
451+
}
452+
453+
var userId = _userService.GetProperUserId(User).Value;
454+
// Select all editable ciphers for this user belonging to the organization
455+
var editableOrgCipherList = (await _cipherRepository.GetManyByUserIdAsync(userId, true))
456+
.Where(c => c.OrganizationId == organizationId && c.UserId == null && c.Edit && c.ViewPassword).ToList();
457+
458+
// Special case for unassigned ciphers
459+
if (await CanAccessUnassignedCiphersAsync(organizationId))
460+
{
461+
var unassignedCiphers =
462+
(await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(
463+
organizationId));
464+
465+
// Users that can access unassigned ciphers can also edit them
466+
editableOrgCipherList.AddRange(unassignedCiphers.Select(c => new CipherDetails(c) { Edit = true }));
467+
}
468+
469+
var editableOrgCiphers = editableOrgCipherList
470+
.ToDictionary(c => c.Id);
471+
472+
if (cipherIds.Any(c => !editableOrgCiphers.ContainsKey(c)))
473+
{
474+
return false;
475+
}
476+
477+
return true;
478+
}
479+
427480
/// <summary>
428481
/// TODO: Move this to its own authorization handler or equivalent service - AC-2062
429482
/// </summary>
@@ -579,7 +632,7 @@ public async Task<OptionalCipherDetailsResponseModel> PutCollections_vNext(Guid
579632
var userId = _userService.GetProperUserId(User).Value;
580633
var cipher = await GetByIdAsync(id, userId);
581634
if (cipher == null || !cipher.OrganizationId.HasValue ||
582-
!await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
635+
!await _currentContext.OrganizationUser(cipher.OrganizationId.Value) || !cipher.ViewPassword)
583636
{
584637
throw new NotFoundException();
585638
}
@@ -634,7 +687,7 @@ public async Task<CipherMiniDetailsResponseModel> PutCollectionsAdmin(string id,
634687
[HttpPost("bulk-collections")]
635688
public async Task PostBulkCollections([FromBody] CipherBulkUpdateCollectionsRequestModel model)
636689
{
637-
if (!await CanEditCiphersAsync(model.OrganizationId, model.CipherIds) ||
690+
if (!await CanModifyCipherCollectionsAsync(model.OrganizationId, model.CipherIds) ||
638691
!await CanEditItemsInCollections(model.OrganizationId, model.CollectionIds))
639692
{
640693
throw new NotFoundException();

src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public async Task<ICollection<CipherDetails>> GetManyByUserIdAsync(Guid userId,
9898

9999
return results
100100
.GroupBy(c => c.Id)
101-
.Select(g => g.OrderByDescending(og => og.Edit).First())
101+
.Select(g => g.OrderByDescending(og => og.Edit).ThenByDescending(og => og.ViewPassword).First())
102102
.ToList();
103103
}
104104
}

src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_ReadByIdUserId.sql

+33-5
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,40 @@ AS
55
BEGIN
66
SET NOCOUNT ON
77

8-
SELECT TOP 1
9-
*
8+
SELECT
9+
[Id],
10+
[UserId],
11+
[OrganizationId],
12+
[Type],
13+
[Data],
14+
[Attachments],
15+
[CreationDate],
16+
[RevisionDate],
17+
[Favorite],
18+
[FolderId],
19+
[DeletedDate],
20+
[Reprompt],
21+
[Key],
22+
[OrganizationUseTotp],
23+
MAX ([Edit]) AS [Edit],
24+
MAX ([ViewPassword]) AS [ViewPassword]
1025
FROM
1126
[dbo].[UserCipherDetails](@UserId)
1227
WHERE
1328
[Id] = @Id
14-
ORDER BY
15-
[Edit] DESC
16-
END
29+
GROUP BY
30+
[Id],
31+
[UserId],
32+
[OrganizationId],
33+
[Type],
34+
[Data],
35+
[Attachments],
36+
[CreationDate],
37+
[RevisionDate],
38+
[Favorite],
39+
[FolderId],
40+
[DeletedDate],
41+
[Reprompt],
42+
[Key],
43+
[OrganizationUseTotp]
44+
END

src/Sql/dbo/Stored Procedures/CollectionCipher_UpdateCollections.sql

+25-31
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@ BEGIN
1414
WHERE
1515
[Id] = @CipherId
1616
)
17-
18-
;WITH [AvailableCollectionsCTE] AS(
19-
SELECT
17+
SELECT
2018
C.[Id]
19+
INTO #TempAvailableCollections
2120
FROM
2221
[dbo].[Collection] C
2322
INNER JOIN
@@ -40,38 +39,33 @@ BEGIN
4039
CU.[ReadOnly] = 0
4140
OR CG.[ReadOnly] = 0
4241
)
43-
),
44-
[CollectionCiphersCTE] AS(
45-
SELECT
46-
[CollectionId],
47-
[CipherId]
48-
FROM
49-
[dbo].[CollectionCipher]
50-
WHERE
51-
[CipherId] = @CipherId
42+
-- Insert new collection assignments
43+
INSERT INTO [dbo].[CollectionCipher] (
44+
[CollectionId],
45+
[CipherId]
5246
)
53-
MERGE
54-
[CollectionCiphersCTE] AS [Target]
55-
USING
56-
@CollectionIds AS [Source]
57-
ON
58-
[Target].[CollectionId] = [Source].[Id]
59-
AND [Target].[CipherId] = @CipherId
60-
WHEN NOT MATCHED BY TARGET
61-
AND [Source].[Id] IN (SELECT [Id] FROM [AvailableCollectionsCTE]) THEN
62-
INSERT VALUES
63-
(
64-
[Source].[Id],
65-
@CipherId
66-
)
67-
WHEN NOT MATCHED BY SOURCE
68-
AND [Target].[CipherId] = @CipherId
69-
AND [Target].[CollectionId] IN (SELECT [Id] FROM [AvailableCollectionsCTE]) THEN
70-
DELETE
71-
;
47+
SELECT
48+
[Id],
49+
@CipherId
50+
FROM @CollectionIds
51+
WHERE [Id] IN (SELECT [Id] FROM [#TempAvailableCollections])
52+
AND NOT EXISTS (
53+
SELECT 1
54+
FROM [dbo].[CollectionCipher]
55+
WHERE [CollectionId] = [@CollectionIds].[Id]
56+
AND [CipherId] = @CipherId
57+
);
58+
59+
-- Delete removed collection assignments
60+
DELETE CC
61+
FROM [dbo].[CollectionCipher] CC
62+
WHERE CC.[CipherId] = @CipherId
63+
AND CC.[CollectionId] IN (SELECT [Id] FROM [#TempAvailableCollections])
64+
AND CC.[CollectionId] NOT IN (SELECT [Id] FROM @CollectionIds);
7265

7366
IF @OrgId IS NOT NULL
7467
BEGIN
7568
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId
7669
END
70+
DROP TABLE #TempAvailableCollections;
7771
END

test/Api.Test/Vault/Controllers/CiphersControllerTests.cs

+1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ private CipherDetails CreateCipherDetailsMock(Guid id, Guid userId)
127127
UserId = userId,
128128
OrganizationId = Guid.NewGuid(),
129129
Type = CipherType.Login,
130+
ViewPassword = true,
130131
Data = @"
131132
{
132133
""Uris"": [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_ReadByIdUserId]
2+
@Id UNIQUEIDENTIFIER,
3+
@UserId UNIQUEIDENTIFIER
4+
AS
5+
BEGIN
6+
SET NOCOUNT ON
7+
8+
SELECT
9+
[Id],
10+
[UserId],
11+
[OrganizationId],
12+
[Type],
13+
[Data],
14+
[Attachments],
15+
[CreationDate],
16+
[RevisionDate],
17+
[Favorite],
18+
[FolderId],
19+
[DeletedDate],
20+
[Reprompt],
21+
[Key],
22+
[OrganizationUseTotp],
23+
MAX ([Edit]) AS [Edit],
24+
MAX ([ViewPassword]) AS [ViewPassword]
25+
FROM
26+
[dbo].[UserCipherDetails](@UserId)
27+
WHERE
28+
[Id] = @Id
29+
GROUP BY
30+
[Id],
31+
[UserId],
32+
[OrganizationId],
33+
[Type],
34+
[Data],
35+
[Attachments],
36+
[CreationDate],
37+
[RevisionDate],
38+
[Favorite],
39+
[FolderId],
40+
[DeletedDate],
41+
[Reprompt],
42+
[Key],
43+
[OrganizationUseTotp]
44+
END
45+
GO
46+
47+
CREATE OR ALTER PROCEDURE [dbo].[CollectionCipher_UpdateCollections]
48+
@CipherId UNIQUEIDENTIFIER,
49+
@UserId UNIQUEIDENTIFIER,
50+
@CollectionIds AS [dbo].[GuidIdArray] READONLY
51+
AS
52+
BEGIN
53+
SET NOCOUNT ON
54+
55+
DECLARE @OrgId UNIQUEIDENTIFIER = (
56+
SELECT TOP 1
57+
[OrganizationId]
58+
FROM
59+
[dbo].[Cipher]
60+
WHERE
61+
[Id] = @CipherId
62+
)
63+
SELECT
64+
C.[Id]
65+
INTO #TempAvailableCollections
66+
FROM
67+
[dbo].[Collection] C
68+
INNER JOIN
69+
[Organization] O ON O.[Id] = C.[OrganizationId]
70+
INNER JOIN
71+
[dbo].[OrganizationUser] OU ON OU.[OrganizationId] = O.[Id] AND OU.[UserId] = @UserId
72+
LEFT JOIN
73+
[dbo].[CollectionUser] CU ON CU.[CollectionId] = C.[Id] AND CU.[OrganizationUserId] = OU.[Id]
74+
LEFT JOIN
75+
[dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]
76+
LEFT JOIN
77+
[dbo].[Group] G ON G.[Id] = GU.[GroupId]
78+
LEFT JOIN
79+
[dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId]
80+
WHERE
81+
O.[Id] = @OrgId
82+
AND O.[Enabled] = 1
83+
AND OU.[Status] = 2 -- Confirmed
84+
AND (
85+
CU.[ReadOnly] = 0
86+
OR CG.[ReadOnly] = 0
87+
)
88+
-- Insert new collection assignments
89+
INSERT INTO [dbo].[CollectionCipher] (
90+
[CollectionId],
91+
[CipherId]
92+
)
93+
SELECT
94+
[Id],
95+
@CipherId
96+
FROM @CollectionIds
97+
WHERE [Id] IN (SELECT [Id] FROM [#TempAvailableCollections])
98+
AND NOT EXISTS (
99+
SELECT 1
100+
FROM [dbo].[CollectionCipher]
101+
WHERE [CollectionId] = [@CollectionIds].[Id]
102+
AND [CipherId] = @CipherId
103+
);
104+
105+
-- Delete removed collection assignments
106+
DELETE CC
107+
FROM [dbo].[CollectionCipher] CC
108+
WHERE CC.[CipherId] = @CipherId
109+
AND CC.[CollectionId] IN (SELECT [Id] FROM [#TempAvailableCollections])
110+
AND CC.[CollectionId] NOT IN (SELECT [Id] FROM @CollectionIds);
111+
112+
IF @OrgId IS NOT NULL
113+
BEGIN
114+
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId
115+
END
116+
DROP TABLE #TempAvailableCollections;
117+
END
118+
GO

0 commit comments

Comments
 (0)