mirror of
https://github.com/bitwarden/server
synced 2025-12-25 12:43:14 +00:00
[PM-25947] Add folders and favorites when sharing a cipher (#6402)
* add folders and favorites when sharing a cipher * refactor folders and favorites assignment to consider existing folders/favorite assignments on a cipher * remove unneeded string manipulation * remove comment * add unit test for folder/favorite sharing * add migration for sharing a cipher to org and collect reprompt, favorite and folders * update date timestamp of migration
This commit is contained in:
@@ -1909,4 +1909,237 @@ public class CiphersControllerTests
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PostPurge(model, organizationId));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutShare_WithNullFolderAndFalseFavorite_UpdatesFieldsCorrectly(
|
||||
Guid cipherId,
|
||||
Guid userId,
|
||||
Guid organizationId,
|
||||
Guid folderId,
|
||||
SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
var user = new User { Id = userId };
|
||||
var userIdKey = userId.ToString().ToUpperInvariant();
|
||||
|
||||
var existingCipher = new Cipher
|
||||
{
|
||||
Id = cipherId,
|
||||
UserId = userId,
|
||||
Type = CipherType.Login,
|
||||
Data = JsonSerializer.Serialize(new { Username = "test", Password = "test" }),
|
||||
Folders = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, folderId.ToString().ToUpperInvariant() } }),
|
||||
Favorites = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, true } })
|
||||
};
|
||||
|
||||
// Clears folder and favorite when sharing
|
||||
var model = new CipherShareRequestModel
|
||||
{
|
||||
Cipher = new CipherRequestModel
|
||||
{
|
||||
Type = CipherType.Login,
|
||||
OrganizationId = organizationId.ToString(),
|
||||
Name = "SharedCipher",
|
||||
Data = JsonSerializer.Serialize(new { Username = "test", Password = "test" }),
|
||||
FolderId = null,
|
||||
Favorite = false,
|
||||
EncryptedFor = userId
|
||||
},
|
||||
CollectionIds = [Guid.NewGuid().ToString()]
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetByIdAsync(cipherId)
|
||||
.Returns(existingCipher);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationUser(organizationId)
|
||||
.Returns(true);
|
||||
|
||||
var sharedCipher = new CipherDetails
|
||||
{
|
||||
Id = cipherId,
|
||||
OrganizationId = organizationId,
|
||||
Type = CipherType.Login,
|
||||
Data = JsonSerializer.Serialize(new { Username = "test", Password = "test" }),
|
||||
FolderId = null,
|
||||
Favorite = false
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetByIdAsync(cipherId, userId)
|
||||
.Returns(sharedCipher);
|
||||
|
||||
sutProvider.GetDependency<IApplicationCacheService>()
|
||||
.GetOrganizationAbilitiesAsync()
|
||||
.Returns(new Dictionary<Guid, OrganizationAbility>
|
||||
{
|
||||
{ organizationId, new OrganizationAbility { Id = organizationId } }
|
||||
});
|
||||
|
||||
var result = await sutProvider.Sut.PutShare(cipherId, model);
|
||||
|
||||
Assert.Null(result.FolderId);
|
||||
Assert.False(result.Favorite);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutShare_WithFolderAndFavoriteSet_AddsUserSpecificFields(
|
||||
Guid cipherId,
|
||||
Guid userId,
|
||||
Guid organizationId,
|
||||
Guid folderId,
|
||||
SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
var user = new User { Id = userId };
|
||||
var userIdKey = userId.ToString().ToUpperInvariant();
|
||||
|
||||
var existingCipher = new Cipher
|
||||
{
|
||||
Id = cipherId,
|
||||
UserId = userId,
|
||||
Type = CipherType.Login,
|
||||
Data = JsonSerializer.Serialize(new { Username = "test", Password = "test" }),
|
||||
Folders = null,
|
||||
Favorites = null
|
||||
};
|
||||
|
||||
// Sets folder and favorite when sharing
|
||||
var model = new CipherShareRequestModel
|
||||
{
|
||||
Cipher = new CipherRequestModel
|
||||
{
|
||||
Type = CipherType.Login,
|
||||
OrganizationId = organizationId.ToString(),
|
||||
Name = "SharedCipher",
|
||||
Data = JsonSerializer.Serialize(new { Username = "test", Password = "test" }),
|
||||
FolderId = folderId.ToString(),
|
||||
Favorite = true,
|
||||
EncryptedFor = userId
|
||||
},
|
||||
CollectionIds = [Guid.NewGuid().ToString()]
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetByIdAsync(cipherId)
|
||||
.Returns(existingCipher);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationUser(organizationId)
|
||||
.Returns(true);
|
||||
|
||||
var sharedCipher = new CipherDetails
|
||||
{
|
||||
Id = cipherId,
|
||||
OrganizationId = organizationId,
|
||||
Type = CipherType.Login,
|
||||
Data = JsonSerializer.Serialize(new { Username = "test", Password = "test" }),
|
||||
Folders = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, folderId.ToString().ToUpperInvariant() } }),
|
||||
Favorites = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, true } }),
|
||||
FolderId = folderId,
|
||||
Favorite = true
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetByIdAsync(cipherId, userId)
|
||||
.Returns(sharedCipher);
|
||||
|
||||
sutProvider.GetDependency<IApplicationCacheService>()
|
||||
.GetOrganizationAbilitiesAsync()
|
||||
.Returns(new Dictionary<Guid, OrganizationAbility>
|
||||
{
|
||||
{ organizationId, new OrganizationAbility { Id = organizationId } }
|
||||
});
|
||||
|
||||
var result = await sutProvider.Sut.PutShare(cipherId, model);
|
||||
|
||||
Assert.Equal(folderId, result.FolderId);
|
||||
Assert.True(result.Favorite);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutShare_UpdateExistingFolderAndFavorite_UpdatesUserSpecificFields(
|
||||
Guid cipherId,
|
||||
Guid userId,
|
||||
Guid organizationId,
|
||||
Guid oldFolderId,
|
||||
Guid newFolderId,
|
||||
SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
var user = new User { Id = userId };
|
||||
var userIdKey = userId.ToString().ToUpperInvariant();
|
||||
|
||||
// Existing cipher with old folder and not favorited
|
||||
var existingCipher = new Cipher
|
||||
{
|
||||
Id = cipherId,
|
||||
UserId = userId,
|
||||
Type = CipherType.Login,
|
||||
Data = JsonSerializer.Serialize(new { Username = "test", Password = "test" }),
|
||||
Folders = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, oldFolderId.ToString().ToUpperInvariant() } }),
|
||||
Favorites = null
|
||||
};
|
||||
|
||||
var model = new CipherShareRequestModel
|
||||
{
|
||||
Cipher = new CipherRequestModel
|
||||
{
|
||||
Type = CipherType.Login,
|
||||
OrganizationId = organizationId.ToString(),
|
||||
Name = "SharedCipher",
|
||||
Data = JsonSerializer.Serialize(new { Username = "test", Password = "test" }),
|
||||
FolderId = newFolderId.ToString(), // Update to new folder
|
||||
Favorite = true, // Add favorite
|
||||
EncryptedFor = userId
|
||||
},
|
||||
CollectionIds = [Guid.NewGuid().ToString()]
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetByIdAsync(cipherId)
|
||||
.Returns(existingCipher);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationUser(organizationId)
|
||||
.Returns(true);
|
||||
|
||||
var sharedCipher = new CipherDetails
|
||||
{
|
||||
Id = cipherId,
|
||||
OrganizationId = organizationId,
|
||||
Type = CipherType.Login,
|
||||
Data = JsonSerializer.Serialize(new { Username = "test", Password = "test" }),
|
||||
Folders = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, newFolderId.ToString().ToUpperInvariant() } }),
|
||||
Favorites = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, true } }),
|
||||
FolderId = newFolderId,
|
||||
Favorite = true
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetByIdAsync(cipherId, userId)
|
||||
.Returns(sharedCipher);
|
||||
|
||||
sutProvider.GetDependency<IApplicationCacheService>()
|
||||
.GetOrganizationAbilitiesAsync()
|
||||
.Returns(new Dictionary<Guid, OrganizationAbility>
|
||||
{
|
||||
{ organizationId, new OrganizationAbility { Id = organizationId } }
|
||||
});
|
||||
|
||||
var result = await sutProvider.Sut.PutShare(cipherId, model);
|
||||
|
||||
Assert.Equal(newFolderId, result.FolderId);
|
||||
Assert.True(result.Favorite);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,4 +225,58 @@ public class CipherRepositoryTests
|
||||
Assert.True(savedCipher == null);
|
||||
}
|
||||
}
|
||||
|
||||
[CiSkippedTheory, EfOrganizationCipherCustomize, BitAutoData]
|
||||
public async Task ReplaceAsync_WithCollections_UpdatesFoldersFavoritesRepromptAndArchivedDateAsync(
|
||||
Cipher cipher,
|
||||
User user,
|
||||
Organization org,
|
||||
Collection collection,
|
||||
List<EfVaultRepo.CipherRepository> suts,
|
||||
List<EfRepo.UserRepository> efUserRepos,
|
||||
List<EfRepo.OrganizationRepository> efOrgRepos,
|
||||
List<EfRepo.CollectionRepository> efCollectionRepos)
|
||||
{
|
||||
foreach (var sut in suts)
|
||||
{
|
||||
var i = suts.IndexOf(sut);
|
||||
|
||||
var postEfOrg = await efOrgRepos[i].CreateAsync(org);
|
||||
efOrgRepos[i].ClearChangeTracking();
|
||||
var postEfUser = await efUserRepos[i].CreateAsync(user);
|
||||
efUserRepos[i].ClearChangeTracking();
|
||||
|
||||
collection.OrganizationId = postEfOrg.Id;
|
||||
var postEfCollection = await efCollectionRepos[i].CreateAsync(collection);
|
||||
efCollectionRepos[i].ClearChangeTracking();
|
||||
|
||||
cipher.UserId = postEfUser.Id;
|
||||
cipher.OrganizationId = null;
|
||||
cipher.Folders = $"{{\"{postEfUser.Id}\":\"some-folder-id\"}}";
|
||||
cipher.Favorites = $"{{\"{postEfUser.Id}\":true}}";
|
||||
cipher.Reprompt = Core.Vault.Enums.CipherRepromptType.Password;
|
||||
|
||||
var createdCipher = await sut.CreateAsync(cipher);
|
||||
sut.ClearChangeTracking();
|
||||
|
||||
var updatedCipher = await sut.GetByIdAsync(createdCipher.Id);
|
||||
updatedCipher.UserId = postEfUser.Id;
|
||||
updatedCipher.OrganizationId = postEfOrg.Id;
|
||||
updatedCipher.Folders = $"{{\"{postEfUser.Id}\":\"new-folder-id\"}}";
|
||||
updatedCipher.Favorites = $"{{\"{postEfUser.Id}\":true}}";
|
||||
updatedCipher.Reprompt = Core.Vault.Enums.CipherRepromptType.Password;
|
||||
|
||||
await sut.ReplaceAsync(updatedCipher, new List<Guid> { postEfCollection.Id });
|
||||
sut.ClearChangeTracking();
|
||||
|
||||
|
||||
var savedCipher = await sut.GetByIdAsync(createdCipher.Id);
|
||||
Assert.NotNull(savedCipher);
|
||||
Assert.Null(savedCipher.UserId);
|
||||
Assert.Equal(postEfOrg.Id, savedCipher.OrganizationId);
|
||||
Assert.Equal($"{{\"{postEfUser.Id}\":\"new-folder-id\"}}", savedCipher.Folders);
|
||||
Assert.Equal($"{{\"{postEfUser.Id}\":true}}", savedCipher.Favorites);
|
||||
Assert.Equal(Core.Vault.Enums.CipherRepromptType.Password, savedCipher.Reprompt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user