1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-22 12:15:36 +01: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

@ -346,6 +346,36 @@ namespace Bit.Api.Controllers
string.IsNullOrWhiteSpace(model.FolderId) ? (Guid?)null : new Guid(model.FolderId), userId);
}
[HttpPut("share")]
[HttpPost("share")]
public async Task PutShareMany([FromBody]CipherBulkShareRequestModel model)
{
var organizationId = new Guid(model.Ciphers.First().OrganizationId);
if(!_currentContext.OrganizationUser(organizationId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User).Value;
var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, false);
var ciphersDict = ciphers.ToDictionary(c => c.Id);
var shareCiphers = new List<Cipher>();
foreach(var cipher in model.Ciphers)
{
var cipherGuid = new Guid(cipher.Id);
if(!ciphersDict.ContainsKey(cipherGuid))
{
throw new BadRequestException("Trying to share ciphers that you do not own.");
}
shareCiphers.Add(cipher.ToCipher(ciphersDict[cipherGuid]));
}
await _cipherService.ShareManyAsync(shareCiphers, organizationId,
model.CollectionIds.Select(c => new Guid(c)), userId);
}
[HttpPost("purge")]
public async Task PostPurge([FromBody]CipherPurgeRequestModel model)
{

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

View File

@ -225,5 +225,6 @@
<Build Include="dbo\Stored Procedures\Organization_Search.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_ReadCountByOrganizationIdEmail.sql" />
<Build Include="dbo\Stored Procedures\CipherDetails_ReadWithoutOrganizationsByUserId.sql" />
<Build Include="dbo\Stored Procedures\CollectionCipher_UpdateCollectionsForCiphers.sql" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,64 @@
CREATE PROCEDURE [dbo].[CollectionCipher_UpdateCollectionsForCiphers]
@CipherIds AS [dbo].[GuidIdArray] READONLY,
@OrganizationId UNIQUEIDENTIFIER,
@UserId UNIQUEIDENTIFIER,
@CollectionIds AS [dbo].[GuidIdArray] READONLY
AS
BEGIN
SET NOCOUNT ON
CREATE TABLE #AvailableCollections (
[Id] UNIQUEIDENTIFIER
)
INSERT INTO #AvailableCollections
SELECT
C.[Id]
FROM
[dbo].[Collection] C
INNER JOIN
[Organization] O ON O.[Id] = C.[OrganizationId]
INNER JOIN
[dbo].[OrganizationUser] OU ON OU.[OrganizationId] = O.[Id] AND OU.[UserId] = @UserId
LEFT JOIN
[dbo].[CollectionUser] CU ON OU.[AccessAll] = 0 AND CU.[CollectionId] = C.[Id] AND CU.[OrganizationUserId] = OU.[Id]
LEFT JOIN
[dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND OU.[AccessAll] = 0 AND GU.[OrganizationUserId] = OU.[Id]
LEFT JOIN
[dbo].[Group] G ON G.[Id] = GU.[GroupId]
LEFT JOIN
[dbo].[CollectionGroup] CG ON G.[AccessAll] = 0 AND CG.[GroupId] = GU.[GroupId]
WHERE
O.[Id] = @OrganizationId
AND O.[Enabled] = 1
AND OU.[Status] = 2 -- Confirmed
AND (
OU.[AccessAll] = 1
OR CU.[ReadOnly] = 0
OR G.[AccessAll] = 1
OR CG.[ReadOnly] = 0
)
IF (SELECT COUNT(1) FROM #AvailableCollections) < 1
BEGIN
-- No writable collections available to share with in this organization.
RETURN
END
INSERT INTO [dbo].[CollectionCipher]
(
[CollectionId],
[CipherId]
)
SELECT
[Collection].[Id],
[Cipher].[Id]
FROM
@CollectionIds [Collection]
INNER JOIN
@CipherIds [Cipher] ON 1 = 1
WHERE
[Collection].[Id] IN (SELECT [Id] FROM #AvailableCollections)
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId
END

View File

@ -12,8 +12,8 @@ BEGIN
OR G.[AccessAll] = 1
OR CU.[ReadOnly] = 0
OR CG.[ReadOnly] = 0
THEN 1
ELSE 0
THEN 0
ELSE 1
END [ReadOnly]
FROM
[dbo].[CollectionView] C

View File

@ -6,13 +6,13 @@ BEGIN
DECLARE @Storage BIGINT
CREATE TABLE #Temp
CREATE TABLE #OrgStorageUpdateTemp
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[Attachments] VARCHAR(MAX) NULL
)
INSERT INTO #Temp
INSERT INTO #OrgStorageUpdateTemp
SELECT
[Id],
[Attachments]
@ -32,14 +32,14 @@ BEGIN
OPENJSON([Attachments])
) [Size]
FROM
#Temp
#OrgStorageUpdateTemp
)
SELECT
@Storage = SUM([Size])
FROM
[CTE]
DROP TABLE #Temp
DROP TABLE #OrgStorageUpdateTemp
UPDATE
[dbo].[Organization]

View File

@ -6,13 +6,13 @@ BEGIN
DECLARE @Storage BIGINT
CREATE TABLE #Temp
CREATE TABLE #UserStorageUpdateTemp
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[Attachments] VARCHAR(MAX) NULL
)
INSERT INTO #Temp
INSERT INTO #UserStorageUpdateTemp
SELECT
[Id],
[Attachments]
@ -31,14 +31,14 @@ BEGIN
OPENJSON([Attachments])
) [Size]
FROM
#Temp
#UserStorageUpdateTemp
)
SELECT
@Storage = SUM([CTE].[Size])
FROM
[CTE]
DROP TABLE #Temp
DROP TABLE #UserStorageUpdateTemp
UPDATE
[dbo].[User]

View File

@ -18,8 +18,8 @@ BEGIN
OR G.[AccessAll] = 1
OR CU.[ReadOnly] = 0
OR CG.[ReadOnly] = 0
THEN 1
ELSE 0
THEN 0
ELSE 1
END [ReadOnly]
FROM
[dbo].[CollectionView] C
@ -62,13 +62,13 @@ BEGIN
DECLARE @Storage BIGINT
CREATE TABLE #Temp
CREATE TABLE #OrgStorageUpdateTemp
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[Attachments] VARCHAR(MAX) NULL
)
INSERT INTO #Temp
INSERT INTO #OrgStorageUpdateTemp
SELECT
[Id],
[Attachments]
@ -88,14 +88,14 @@ BEGIN
OPENJSON([Attachments])
) [Size]
FROM
#Temp
#OrgStorageUpdateTemp
)
SELECT
@Storage = SUM([Size])
FROM
[CTE]
DROP TABLE #Temp
DROP TABLE #OrgStorageUpdateTemp
UPDATE
[dbo].[Organization]
@ -121,13 +121,13 @@ BEGIN
DECLARE @Storage BIGINT
CREATE TABLE #Temp
CREATE TABLE #UserStorageUpdateTemp
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[Attachments] VARCHAR(MAX) NULL
)
INSERT INTO #Temp
INSERT INTO #UserStorageUpdateTemp
SELECT
[Id],
[Attachments]
@ -146,14 +146,14 @@ BEGIN
OPENJSON([Attachments])
) [Size]
FROM
#Temp
#UserStorageUpdateTemp
)
SELECT
@Storage = SUM([CTE].[Size])
FROM
[CTE]
DROP TABLE #Temp
DROP TABLE #UserStorageUpdateTemp
UPDATE
[dbo].[User]
@ -164,3 +164,75 @@ BEGIN
[Id] = @Id
END
GO
IF OBJECT_ID('[dbo].[CollectionCipher_UpdateCollectionsForCiphers]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[CollectionCipher_UpdateCollectionsForCiphers]
END
GO
CREATE PROCEDURE [dbo].[CollectionCipher_UpdateCollectionsForCiphers]
@CipherIds AS [dbo].[GuidIdArray] READONLY,
@OrganizationId UNIQUEIDENTIFIER,
@UserId UNIQUEIDENTIFIER,
@CollectionIds AS [dbo].[GuidIdArray] READONLY
AS
BEGIN
SET NOCOUNT ON
CREATE TABLE #AvailableCollections (
[Id] UNIQUEIDENTIFIER
)
INSERT INTO #AvailableCollections
SELECT
C.[Id]
FROM
[dbo].[Collection] C
INNER JOIN
[Organization] O ON O.[Id] = C.[OrganizationId]
INNER JOIN
[dbo].[OrganizationUser] OU ON OU.[OrganizationId] = O.[Id] AND OU.[UserId] = @UserId
LEFT JOIN
[dbo].[CollectionUser] CU ON OU.[AccessAll] = 0 AND CU.[CollectionId] = C.[Id] AND CU.[OrganizationUserId] = OU.[Id]
LEFT JOIN
[dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND OU.[AccessAll] = 0 AND GU.[OrganizationUserId] = OU.[Id]
LEFT JOIN
[dbo].[Group] G ON G.[Id] = GU.[GroupId]
LEFT JOIN
[dbo].[CollectionGroup] CG ON G.[AccessAll] = 0 AND CG.[GroupId] = GU.[GroupId]
WHERE
O.[Id] = @OrganizationId
AND O.[Enabled] = 1
AND OU.[Status] = 2 -- Confirmed
AND (
OU.[AccessAll] = 1
OR CU.[ReadOnly] = 0
OR G.[AccessAll] = 1
OR CG.[ReadOnly] = 0
)
IF (SELECT COUNT(1) FROM #AvailableCollections) < 1
BEGIN
-- No writable collections available to share with in this organization.
RETURN
END
INSERT INTO [dbo].[CollectionCipher]
(
[CollectionId],
[CipherId]
)
SELECT
[Collection].[Id],
[Cipher].[Id]
FROM
@CollectionIds [Collection]
INNER JOIN
@CipherIds [Cipher] ON 1 = 1
WHERE
[Collection].[Id] IN (SELECT [Id] FROM #AvailableCollections)
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId
END
GO