1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-17 15:40:59 -05:00

apis for bulk sharing

This commit is contained in:
Kyle Spearrin
2018-06-13 14:03:44 -04:00
parent ebb1f9e1a8
commit de552be25f
14 changed files with 381 additions and 20 deletions

View File

@ -182,4 +182,50 @@ namespace Bit.Core.Models.Api
public IEnumerable<string> Ids { get; set; }
public string FolderId { get; set; }
}
public class CipherBulkShareRequestModel
{
[Required]
public IEnumerable<string> CollectionIds { get; set; }
[Required]
public IEnumerable<CipherWithIdRequestModel> Ciphers { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if(!Ciphers?.Any() ?? false)
{
yield return new ValidationResult("You must select at least one cipher.",
new string[] { nameof(Ciphers) });
}
else
{
var allHaveIds = true;
var organizationIds = new HashSet<string>();
foreach(var c in Ciphers)
{
organizationIds.Add(c.OrganizationId);
if(allHaveIds)
{
allHaveIds = !(string.IsNullOrWhiteSpace(c.Id) || string.IsNullOrWhiteSpace(c.OrganizationId));
}
}
if(!allHaveIds)
{
yield return new ValidationResult("All Ciphers must have an Id and OrganizationId.",
new string[] { nameof(Ciphers) });
}
else if(organizationIds.Count != 1)
{
yield return new ValidationResult("All ciphers must be for the same organization.");
}
}
if(!CollectionIds?.Any() ?? false)
{
yield return new ValidationResult("You must select at least one collection.",
new string[] { nameof(CollectionIds) });
}
}
}
}

View File

@ -25,6 +25,7 @@ namespace Bit.Core.Repositories
Task MoveAsync(IEnumerable<Guid> ids, Guid? folderId, Guid userId);
Task DeleteByUserIdAsync(Guid userId);
Task UpdateUserKeysAndCiphersAsync(User user, IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);
Task UpdateCiphersAsync(Guid userId, IEnumerable<Cipher> ciphers);
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
IEnumerable<CollectionCipher> collectionCiphers);

View File

@ -12,5 +12,7 @@ namespace Bit.Core.Repositories
Task<ICollection<CollectionCipher>> GetManyByUserIdCipherIdAsync(Guid userId, Guid cipherId);
Task UpdateCollectionsAsync(Guid cipherId, Guid userId, IEnumerable<Guid> collectionIds);
Task UpdateCollectionsForAdminAsync(Guid cipherId, Guid organizationId, IEnumerable<Guid> collectionIds);
Task UpdateCollectionsForCiphersAsync(IEnumerable<Guid> cipherIds, Guid userId, Guid organizationId,
IEnumerable<Guid> collectionIds);
}
}

View File

@ -346,6 +346,86 @@ namespace Bit.Core.Repositories.SqlServer
return Task.FromResult(0);
}
public async Task UpdateCiphersAsync(Guid userId, IEnumerable<Cipher> ciphers)
{
if(!ciphers.Any())
{
return;
}
using(var connection = new SqlConnection(ConnectionString))
{
connection.Open();
using(var transaction = connection.BeginTransaction())
{
try
{
// 1. Create temp tables to bulk copy into.
var sqlCreateTemp = @"
SELECT TOP 0 *
INTO #TempCipher
FROM [dbo].[Cipher]";
using(var cmd = new SqlCommand(sqlCreateTemp, connection, transaction))
{
cmd.ExecuteNonQuery();
}
// 2. Bulk copy into temp tables.
using(var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
{
bulkCopy.DestinationTableName = "#TempCipher";
var dataTable = BuildCiphersTable(ciphers);
bulkCopy.WriteToServer(dataTable);
}
// 3. Insert into real tables from temp tables and clean up.
// Intentionally not including Favorites, Folders, and CreationDate
// since those are not meant to be bulk updated at this time
var sql = @"
UPDATE
[dbo].[Cipher]
SET
[UserId] = TC.[UserId],
[OrganizationId] = TC.[OrganizationId],
[Type] = TC.[Type],
[Data] = TC.[Data],
[Attachments] = TC.[Attachments],
[RevisionDate] = TC.[RevisionDate]
FROM
[dbo].[Cipher] C
INNER JOIN
#TempCipher TC ON C.Id = TC.Id
WHERE
C.[UserId] = @UserId
DROP TABLE #TempCipher";
using(var cmd = new SqlCommand(sql, connection, transaction))
{
cmd.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier).Value = userId;
cmd.ExecuteNonQuery();
}
await connection.ExecuteAsync(
$"[{Schema}].[User_BumpAccountRevisionDate]",
new { Id = userId },
commandType: CommandType.StoredProcedure, transaction: transaction);
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
}
}
public async Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders)
{
if(!ciphers.Any())

View File

@ -80,5 +80,23 @@ namespace Bit.Core.Repositories.SqlServer
commandType: CommandType.StoredProcedure);
}
}
public async Task UpdateCollectionsForCiphersAsync(IEnumerable<Guid> cipherIds, Guid userId,
Guid organizationId, IEnumerable<Guid> collectionIds)
{
using(var connection = new SqlConnection(ConnectionString))
{
var results = await connection.ExecuteAsync(
"[dbo].[CollectionCipher_UpdateCollectionsForCiphers]",
new
{
CipherIds = cipherIds.ToGuidIdArrayTVP(),
UserId = userId,
OrganizationId = organizationId,
CollectionIds = collectionIds.ToGuidIdArrayTVP()
},
commandType: CommandType.StoredProcedure);
}
}
}
}

View File

@ -22,6 +22,7 @@ namespace Bit.Core.Services
Task SaveFolderAsync(Folder folder);
Task DeleteFolderAsync(Folder folder);
Task ShareAsync(Cipher originalCipher, Cipher cipher, Guid organizationId, IEnumerable<Guid> collectionIds, Guid userId);
Task ShareManyAsync(IEnumerable<Cipher> ciphers, Guid organizationId, IEnumerable<Guid> collectionIds, Guid sharingUserId);
Task SaveCollectionsAsync(Cipher cipher, IEnumerable<Guid> collectionIds, Guid savingUserId, bool orgAdmin);
Task ImportCiphersAsync(List<Folder> folders, List<CipherDetails> ciphers,
IEnumerable<KeyValuePair<int, int>> folderRelationships);

View File

@ -401,6 +401,52 @@ namespace Bit.Core.Services
await _pushService.PushSyncCipherUpdateAsync(cipher);
}
public async Task ShareManyAsync(IEnumerable<Cipher> ciphers, Guid organizationId,
IEnumerable<Guid> collectionIds, Guid sharingUserId)
{
var cipherIds = new List<Guid>();
foreach(var cipher in ciphers)
{
if(cipher.Id == default(Guid))
{
throw new BadRequestException("All ciphers must already exist.");
}
if(cipher.OrganizationId.HasValue)
{
throw new BadRequestException("One or more ciphers already belong to an organization.");
}
if(!cipher.UserId.HasValue || cipher.UserId.Value != sharingUserId)
{
throw new BadRequestException("One or more ciphers do not belong to you.");
}
if(!string.IsNullOrWhiteSpace(cipher.Attachments))
{
throw new BadRequestException("One or more ciphers have attachments.");
}
cipher.UserId = null;
cipher.OrganizationId = organizationId;
cipher.RevisionDate = DateTime.UtcNow;
cipherIds.Add(cipher.Id);
}
await _cipherRepository.UpdateCiphersAsync(sharingUserId, ciphers);
await _collectionCipherRepository.UpdateCollectionsForCiphersAsync(cipherIds, sharingUserId,
organizationId, collectionIds);
// TODO: move this to a single event?
foreach(var cipher in ciphers)
{
await _eventService.LogCipherEventAsync(cipher, Enums.EventType.Cipher_Shared);
}
// push
await _pushService.PushSyncCiphersAsync(sharingUserId);
}
public async Task SaveCollectionsAsync(Cipher cipher, IEnumerable<Guid> collectionIds, Guid savingUserId, bool orgAdmin)
{
if(cipher.Id == default(Guid))