1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 07:36:14 -05:00

[PM-18085] Add Manage property to UserCipherDetails (#5390)

* Add Manage permission to UserCipherDetails and CipherDetails_ReadByIdUserId

* Add Manage property to CipherDetails and UserCipherDetailsQuery

* Add integration test for CipherRepository Manage permission rules

* Update CipherDetails_ReadWithoutOrganizationsByUserId to include Manage permission

* Refactor UserCipherDetailsQuery to include detailed permission and organization properties

* Refactor CipherRepositoryTests to improve test organization and readability

- Split large test method into smaller, focused methods
- Added helper methods for creating test data and performing assertions
- Improved test coverage for cipher permissions in different scenarios
- Maintained existing test logic while enhancing code structure

* Refactor CipherRepositoryTests to consolidate cipher permission tests

- Removed redundant helper methods for permission assertions
- Simplified test methods for GetCipherPermissionsForOrganizationAsync, GetManyByUserIdAsync, and GetByIdAsync
- Maintained existing test coverage for cipher manage permissions
- Improved code readability and reduced code duplication

* Add integration test for CipherRepository group collection manage permissions

- Added new test method GetCipherPermissionsForOrganizationAsync_ManageProperty_RespectsCollectionGroupRules
- Implemented helper method CreateCipherInOrganizationCollectionWithGroup to support group-based collection permission testing
- Verified manage permissions are correctly applied based on group collection access settings

* Add @Manage parameter to Cipher stored procedures

- Updated CipherDetails_Create, CipherDetails_CreateWithCollections, and CipherDetails_Update stored procedures
- Added @Manage parameter with comment "-- not used"
- Included new stored procedure implementations in migration script
- Consistent with previous work on adding Manage property to cipher details

* Update UserCipherDetails functions to reorder Manage and ViewPassword columns

* Reorder Manage and ViewPassword properties in cipher details queries

* Bump date in migration script
This commit is contained in:
Rui Tomé
2025-02-24 11:40:53 +00:00
committed by GitHub
parent b0c6fc9146
commit 2b1db97d5c
11 changed files with 645 additions and 12 deletions

View File

@ -433,4 +433,275 @@ public class CipherRepositoryTests
Assert.False(unassignedCipherPermission.Read);
Assert.False(unassignedCipherPermission.ViewPassword);
}
[DatabaseTheory, DatabaseData]
public async Task GetCipherPermissionsForOrganizationAsync_ManageProperty_RespectsCollectionUserRules(
ICipherRepository cipherRepository,
IUserRepository userRepository,
ICollectionCipherRepository collectionCipherRepository,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository)
{
var (user, organization, orgUser) = await CreateTestUserAndOrganization(userRepository, organizationRepository, organizationUserRepository);
var manageCipher = await CreateCipherInOrganizationCollection(
organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository,
hasManagePermission: true, "Manage Collection");
var nonManageCipher = await CreateCipherInOrganizationCollection(
organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository,
hasManagePermission: false, "Non-Manage Collection");
var permissions = await cipherRepository.GetCipherPermissionsForOrganizationAsync(organization.Id, user.Id);
Assert.Equal(2, permissions.Count);
var managePermission = permissions.FirstOrDefault(c => c.Id == manageCipher.Id);
Assert.NotNull(managePermission);
Assert.True(managePermission.Manage, "Collection with Manage=true should grant Manage permission");
var nonManagePermission = permissions.FirstOrDefault(c => c.Id == nonManageCipher.Id);
Assert.NotNull(nonManagePermission);
Assert.False(nonManagePermission.Manage, "Collection with Manage=false should not grant Manage permission");
}
[DatabaseTheory, DatabaseData]
public async Task GetCipherPermissionsForOrganizationAsync_ManageProperty_RespectsCollectionGroupRules(
ICipherRepository cipherRepository,
IUserRepository userRepository,
ICollectionCipherRepository collectionCipherRepository,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IGroupRepository groupRepository)
{
var (user, organization, orgUser) = await CreateTestUserAndOrganization(userRepository, organizationRepository, organizationUserRepository);
var group = await groupRepository.CreateAsync(new Group
{
OrganizationId = organization.Id,
Name = "Test Group",
});
await groupRepository.UpdateUsersAsync(group.Id, new[] { orgUser.Id });
var (manageCipher, nonManageCipher) = await CreateCipherInOrganizationCollectionWithGroup(
organization, group, cipherRepository, collectionRepository, collectionCipherRepository, groupRepository);
var permissions = await cipherRepository.GetCipherPermissionsForOrganizationAsync(organization.Id, user.Id);
Assert.Equal(2, permissions.Count);
var managePermission = permissions.FirstOrDefault(c => c.Id == manageCipher.Id);
Assert.NotNull(managePermission);
Assert.True(managePermission.Manage, "Collection with Group Manage=true should grant Manage permission");
var nonManagePermission = permissions.FirstOrDefault(c => c.Id == nonManageCipher.Id);
Assert.NotNull(nonManagePermission);
Assert.False(nonManagePermission.Manage, "Collection with Group Manage=false should not grant Manage permission");
}
[DatabaseTheory, DatabaseData]
public async Task GetManyByUserIdAsync_ManageProperty_RespectsCollectionAndOwnershipRules(
ICipherRepository cipherRepository,
IUserRepository userRepository,
ICollectionCipherRepository collectionCipherRepository,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository)
{
var (user, organization, orgUser) = await CreateTestUserAndOrganization(userRepository, organizationRepository, organizationUserRepository);
var manageCipher = await CreateCipherInOrganizationCollection(
organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository,
hasManagePermission: true, "Manage Collection");
var nonManageCipher = await CreateCipherInOrganizationCollection(
organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository,
hasManagePermission: false, "Non-Manage Collection");
var personalCipher = await CreatePersonalCipher(user, cipherRepository);
var userCiphers = await cipherRepository.GetManyByUserIdAsync(user.Id);
Assert.Equal(3, userCiphers.Count);
var managePermission = userCiphers.FirstOrDefault(c => c.Id == manageCipher.Id);
Assert.NotNull(managePermission);
Assert.True(managePermission.Manage, "Collection with Manage=true should grant Manage permission");
var nonManagePermission = userCiphers.FirstOrDefault(c => c.Id == nonManageCipher.Id);
Assert.NotNull(nonManagePermission);
Assert.False(nonManagePermission.Manage, "Collection with Manage=false should not grant Manage permission");
var personalPermission = userCiphers.FirstOrDefault(c => c.Id == personalCipher.Id);
Assert.NotNull(personalPermission);
Assert.True(personalPermission.Manage, "Personal ciphers should always have Manage permission");
}
[DatabaseTheory, DatabaseData]
public async Task GetByIdAsync_ManageProperty_RespectsCollectionAndOwnershipRules(
ICipherRepository cipherRepository,
IUserRepository userRepository,
ICollectionCipherRepository collectionCipherRepository,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository)
{
var (user, organization, orgUser) = await CreateTestUserAndOrganization(userRepository, organizationRepository, organizationUserRepository);
var manageCipher = await CreateCipherInOrganizationCollection(
organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository,
hasManagePermission: true, "Manage Collection");
var nonManageCipher = await CreateCipherInOrganizationCollection(
organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository,
hasManagePermission: false, "Non-Manage Collection");
var personalCipher = await CreatePersonalCipher(user, cipherRepository);
var manageDetails = await cipherRepository.GetByIdAsync(manageCipher.Id, user.Id);
Assert.NotNull(manageDetails);
Assert.True(manageDetails.Manage, "Collection with Manage=true should grant Manage permission");
var nonManageDetails = await cipherRepository.GetByIdAsync(nonManageCipher.Id, user.Id);
Assert.NotNull(nonManageDetails);
Assert.False(nonManageDetails.Manage, "Collection with Manage=false should not grant Manage permission");
var personalDetails = await cipherRepository.GetByIdAsync(personalCipher.Id, user.Id);
Assert.NotNull(personalDetails);
Assert.True(personalDetails.Manage, "Personal ciphers should always have Manage permission");
}
private async Task<(User user, Organization org, OrganizationUser orgUser)> CreateTestUserAndOrganization(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository)
{
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Organization",
BillingEmail = user.Email,
Plan = "Test"
});
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
{
UserId = user.Id,
OrganizationId = organization.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Owner,
});
return (user, organization, orgUser);
}
private async Task<Cipher> CreateCipherInOrganizationCollection(
Organization organization,
OrganizationUser orgUser,
ICipherRepository cipherRepository,
ICollectionRepository collectionRepository,
ICollectionCipherRepository collectionCipherRepository,
bool hasManagePermission,
string collectionName)
{
var collection = await collectionRepository.CreateAsync(new Collection
{
Name = collectionName,
OrganizationId = organization.Id,
});
var cipher = await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
OrganizationId = organization.Id,
Data = ""
});
await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipher.Id, organization.Id,
new List<Guid> { collection.Id });
await collectionRepository.UpdateUsersAsync(collection.Id, new List<CollectionAccessSelection>
{
new() { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = hasManagePermission }
});
return cipher;
}
private async Task<(Cipher manageCipher, Cipher nonManageCipher)> CreateCipherInOrganizationCollectionWithGroup(
Organization organization,
Group group,
ICipherRepository cipherRepository,
ICollectionRepository collectionRepository,
ICollectionCipherRepository collectionCipherRepository,
IGroupRepository groupRepository)
{
var manageCollection = await collectionRepository.CreateAsync(new Collection
{
Name = "Group Manage Collection",
OrganizationId = organization.Id,
});
var nonManageCollection = await collectionRepository.CreateAsync(new Collection
{
Name = "Group Non-Manage Collection",
OrganizationId = organization.Id,
});
var manageCipher = await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
OrganizationId = organization.Id,
Data = ""
});
var nonManageCipher = await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
OrganizationId = organization.Id,
Data = ""
});
await collectionCipherRepository.UpdateCollectionsForAdminAsync(manageCipher.Id, organization.Id,
new List<Guid> { manageCollection.Id });
await collectionCipherRepository.UpdateCollectionsForAdminAsync(nonManageCipher.Id, organization.Id,
new List<Guid> { nonManageCollection.Id });
await groupRepository.ReplaceAsync(group,
new[]
{
new CollectionAccessSelection
{
Id = manageCollection.Id,
HidePasswords = false,
ReadOnly = false,
Manage = true
},
new CollectionAccessSelection
{
Id = nonManageCollection.Id,
HidePasswords = false,
ReadOnly = false,
Manage = false
}
});
return (manageCipher, nonManageCipher);
}
private async Task<Cipher> CreatePersonalCipher(User user, ICipherRepository cipherRepository)
{
return await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
UserId = user.Id,
Data = ""
});
}
}