diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs
index ce517bac7f..faa753abd2 100644
--- a/src/Api/Controllers/CiphersController.cs
+++ b/src/Api/Controllers/CiphersController.cs
@@ -88,6 +88,21 @@ namespace Bit.Api.Controllers
// await _cipherService.SaveAsync(cipher);
//}
+ [HttpPut("{id}/move")]
+ [HttpPost("{id}/move")]
+ public async Task PostMoveSubvault(string id, [FromBody]CipherMoveRequestModel model)
+ {
+ var userId = _userService.GetProperUserId(User).Value;
+ var cipher = await _cipherRepository.GetByIdAsync(new Guid(id));
+ if(cipher == null)
+ {
+ throw new NotFoundException();
+ }
+
+ await _cipherService.MoveSubvaultAsync(model.Cipher.ToCipher(cipher),
+ model.SubvaultIds.Select(s => new Guid(s)), userId);
+ }
+
[HttpDelete("{id}")]
[HttpPost("{id}/delete")]
public async Task Delete(string id)
diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj
index 6e71211605..cddb6183a6 100644
--- a/src/Core/Core.csproj
+++ b/src/Core/Core.csproj
@@ -19,6 +19,7 @@
+
diff --git a/src/Core/Models/Api/Request/CipherRequestModel.cs b/src/Core/Models/Api/Request/CipherRequestModel.cs
index 17c492c799..04c77e0b14 100644
--- a/src/Core/Models/Api/Request/CipherRequestModel.cs
+++ b/src/Core/Models/Api/Request/CipherRequestModel.cs
@@ -4,6 +4,7 @@ using Bit.Core.Utilities;
using Bit.Core.Models.Table;
using Bit.Core.Enums;
using Newtonsoft.Json;
+using System.Collections.Generic;
namespace Bit.Core.Models.Api
{
@@ -15,6 +16,8 @@ namespace Bit.Core.Models.Api
[StringLength(36)]
public string Id { get; set; }
[StringLength(36)]
+ public string OrganizationId { get; set; }
+ [StringLength(36)]
public string FolderId { get; set; }
[Required]
[EncryptedString]
@@ -35,24 +38,35 @@ namespace Bit.Core.Models.Api
public virtual Cipher ToCipher(Guid userId)
{
- var cipher = new Cipher
+ return ToCipher(new Cipher
{
Id = new Guid(Id),
- UserId = userId,
- //FolderId = string.IsNullOrWhiteSpace(FolderId) ? null : (Guid?)new Guid(FolderId),
+ UserId = string.IsNullOrWhiteSpace(OrganizationId) ? (Guid?)userId : null,
Type = Type
- };
+ });
+ }
- switch(Type)
+ public Cipher ToCipher(Cipher existingCipher)
+ {
+ existingCipher.OrganizationId = string.IsNullOrWhiteSpace(OrganizationId) ? null : (Guid?)new Guid(OrganizationId);
+
+ switch(existingCipher.Type)
{
case CipherType.Login:
- cipher.Data = JsonConvert.SerializeObject(new LoginDataModel(this), new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
+ existingCipher.Data = JsonConvert.SerializeObject(new LoginDataModel(this), new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
break;
default:
throw new ArgumentException("Unsupported " + nameof(Type) + ".");
}
- return cipher;
+ return existingCipher;
}
}
+
+ public class CipherMoveRequestModel
+ {
+ public IEnumerable SubvaultIds { get; set; }
+ [Required]
+ public CipherRequestModel Cipher { get; set; }
+ }
}
diff --git a/src/Core/Models/Api/Response/CipherResponseModel.cs b/src/Core/Models/Api/Response/CipherResponseModel.cs
index 2bc0a626c5..6486892b86 100644
--- a/src/Core/Models/Api/Response/CipherResponseModel.cs
+++ b/src/Core/Models/Api/Response/CipherResponseModel.cs
@@ -1,5 +1,8 @@
using System;
using Core.Models.Data;
+using System.Collections.Generic;
+using Bit.Core.Models.Table;
+using System.Linq;
namespace Bit.Core.Models.Api
{
@@ -16,6 +19,7 @@ namespace Bit.Core.Models.Api
Id = cipher.Id.ToString();
Type = cipher.Type;
RevisionDate = cipher.RevisionDate;
+ OrganizationId = cipher.OrganizationId?.ToString();
FolderId = cipher.FolderId?.ToString();
Favorite = cipher.Favorite;
@@ -30,10 +34,22 @@ namespace Bit.Core.Models.Api
}
public string Id { get; set; }
+ public string OrganizationId { get; set; }
public string FolderId { get; set; }
public Enums.CipherType Type { get; set; }
public bool Favorite { get; set; }
public dynamic Data { get; set; }
public DateTime RevisionDate { get; set; }
}
+
+ public class CipherDetailsResponseModel : CipherResponseModel
+ {
+ public CipherDetailsResponseModel(CipherDetails cipher, IEnumerable subvaultCipher)
+ : base(cipher, "cipherDetails")
+ {
+ SubvaultIds = subvaultCipher.Select(s => s.SubvaultId);
+ }
+
+ public IEnumerable SubvaultIds { get; set; }
+ }
}
diff --git a/src/Core/Models/Api/Response/LoginResponseModel.cs b/src/Core/Models/Api/Response/LoginResponseModel.cs
index d547b9b00c..cc2f2282f2 100644
--- a/src/Core/Models/Api/Response/LoginResponseModel.cs
+++ b/src/Core/Models/Api/Response/LoginResponseModel.cs
@@ -21,6 +21,7 @@ namespace Bit.Core.Models.Api
var data = new LoginDataModel(cipher);
Id = cipher.Id.ToString();
+ OrganizationId = cipher.OrganizationId?.ToString();
FolderId = cipher.FolderId?.ToString();
Favorite = cipher.Favorite;
Name = data.Name;
@@ -32,6 +33,7 @@ namespace Bit.Core.Models.Api
}
public string Id { get; set; }
+ public string OrganizationId { get; set; }
public string FolderId { get; set; }
public bool Favorite { get; set; }
public string Name { get; set; }
diff --git a/src/Core/Models/Data/SubvaultUserPermissions.cs b/src/Core/Models/Data/SubvaultUserPermissions.cs
new file mode 100644
index 0000000000..0f121359df
--- /dev/null
+++ b/src/Core/Models/Data/SubvaultUserPermissions.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Bit.Core.Models.Data
+{
+ public class SubvaultUserPermissions
+ {
+ public Guid SubvaultId { get; set; }
+ public bool ReadOnly { get; set; }
+ public bool Admin { get; set; }
+ }
+}
diff --git a/src/Core/Models/Table/Cipher.cs b/src/Core/Models/Table/Cipher.cs
index 0d7cc3d04b..94d386afeb 100644
--- a/src/Core/Models/Table/Cipher.cs
+++ b/src/Core/Models/Table/Cipher.cs
@@ -6,7 +6,7 @@ namespace Bit.Core.Models.Table
public class Cipher : IDataObject
{
public Guid Id { get; set; }
- public Guid UserId { get; set; }
+ public Guid? UserId { get; set; }
public Guid? OrganizationId { get; set; }
public Enums.CipherType Type { get; set; }
public string Data { get; set; }
diff --git a/src/Core/Models/Table/SubvaultCipher.cs b/src/Core/Models/Table/SubvaultCipher.cs
new file mode 100644
index 0000000000..ae87ab41fc
--- /dev/null
+++ b/src/Core/Models/Table/SubvaultCipher.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace Bit.Core.Models.Table
+{
+ public class SubvaultCipher
+ {
+ public Guid SubvaultId { get; set; }
+ public Guid CipherId { get; set; }
+ }
+}
diff --git a/src/Core/Repositories/ICipherRepository.cs b/src/Core/Repositories/ICipherRepository.cs
index 892e5f94cd..5f7e9b776c 100644
--- a/src/Core/Repositories/ICipherRepository.cs
+++ b/src/Core/Repositories/ICipherRepository.cs
@@ -16,6 +16,7 @@ namespace Bit.Core.Repositories
Task CreateAsync(CipherDetails cipher);
Task ReplaceAsync(CipherDetails cipher);
Task UpsertAsync(CipherDetails cipher);
+ Task ReplaceAsync(Cipher obj, IEnumerable subvaultIds);
Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable ciphers);
Task CreateAsync(IEnumerable ciphers);
}
diff --git a/src/Core/Repositories/ISubvaultUserRepository.cs b/src/Core/Repositories/ISubvaultUserRepository.cs
index ce682ff7e1..883a118f3d 100644
--- a/src/Core/Repositories/ISubvaultUserRepository.cs
+++ b/src/Core/Repositories/ISubvaultUserRepository.cs
@@ -2,11 +2,15 @@
using System.Threading.Tasks;
using Bit.Core.Models.Table;
using System.Collections.Generic;
+using Bit.Core.Models.Data;
namespace Bit.Core.Repositories
{
public interface ISubvaultUserRepository : IRepository
{
Task> GetManyByOrganizationUserIdAsync(Guid orgUserId);
+ Task> GetManyDetailsByUserIdAsync(Guid userId);
+ Task> GetPermissionsByUserIdAsync(Guid userId, IEnumerable subvaultIds,
+ Guid organizationId);
}
}
diff --git a/src/Core/Repositories/SqlServer/CipherRepository.cs b/src/Core/Repositories/SqlServer/CipherRepository.cs
index eb9abde8f3..d1e77f808f 100644
--- a/src/Core/Repositories/SqlServer/CipherRepository.cs
+++ b/src/Core/Repositories/SqlServer/CipherRepository.cs
@@ -8,6 +8,8 @@ using Bit.Core.Models.Table;
using System.Data;
using Dapper;
using Core.Models.Data;
+using Bit.Core.Utilities;
+using Newtonsoft.Json;
namespace Bit.Core.Repositories.SqlServer
{
@@ -120,6 +122,20 @@ namespace Bit.Core.Repositories.SqlServer
}
}
+ public async Task ReplaceAsync(Cipher obj, IEnumerable subvaultIds)
+ {
+ var objWithSubvaults = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj));
+ objWithSubvaults.SubvaultIds = subvaultIds.ToGuidIdArrayTVP();
+
+ using(var connection = new SqlConnection(ConnectionString))
+ {
+ var results = await connection.ExecuteAsync(
+ $"[{Schema}].[Cipher_UpdateWithSubvaults]",
+ objWithSubvaults,
+ commandType: CommandType.StoredProcedure);
+ }
+ }
+
public Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable ciphers)
{
if(ciphers.Count() == 0)
@@ -252,5 +268,10 @@ namespace Bit.Core.Repositories.SqlServer
return Task.FromResult(0);
}
+
+ public class CipherWithSubvaults : Cipher
+ {
+ public DataTable SubvaultIds { get; set; }
+ }
}
}
diff --git a/src/Core/Repositories/SqlServer/SubvaultUserRepository.cs b/src/Core/Repositories/SqlServer/SubvaultUserRepository.cs
index 565d16f82a..fecb589239 100644
--- a/src/Core/Repositories/SqlServer/SubvaultUserRepository.cs
+++ b/src/Core/Repositories/SqlServer/SubvaultUserRepository.cs
@@ -6,6 +6,8 @@ using System.Data;
using System.Data.SqlClient;
using Dapper;
using System.Linq;
+using Bit.Core.Models.Data;
+using Bit.Core.Utilities;
namespace Bit.Core.Repositories.SqlServer
{
@@ -31,5 +33,32 @@ namespace Bit.Core.Repositories.SqlServer
return results.ToList();
}
}
+
+ public async Task> GetManyDetailsByUserIdAsync(Guid userId)
+ {
+ using(var connection = new SqlConnection(ConnectionString))
+ {
+ var results = await connection.QueryAsync(
+ $"[{Schema}].[SubvaultUserDetails_ReadByUserId]",
+ new { UserId = userId },
+ commandType: CommandType.StoredProcedure);
+
+ return results.ToList();
+ }
+ }
+
+ public async Task> GetPermissionsByUserIdAsync(Guid userId,
+ IEnumerable subvaultIds, Guid organizationId)
+ {
+ using(var connection = new SqlConnection(ConnectionString))
+ {
+ var results = await connection.QueryAsync(
+ $"[{Schema}].[SubvaultUser_ReadPermissionsBySubvaultUserId]",
+ new { UserId = userId, SubvaultIds = subvaultIds.ToGuidIdArrayTVP(), OrganizationId = organizationId },
+ commandType: CommandType.StoredProcedure);
+
+ return results.ToList();
+ }
+ }
}
}
diff --git a/src/Core/Services/ICipherService.cs b/src/Core/Services/ICipherService.cs
index 0afe7e35ed..1bfdbf6d7f 100644
--- a/src/Core/Services/ICipherService.cs
+++ b/src/Core/Services/ICipherService.cs
@@ -2,6 +2,7 @@
using System.Threading.Tasks;
using Bit.Core.Models.Table;
using Core.Models.Data;
+using System;
namespace Bit.Core.Services
{
@@ -11,6 +12,7 @@ namespace Bit.Core.Services
Task DeleteAsync(Cipher cipher);
Task SaveFolderAsync(Folder folder);
Task DeleteFolderAsync(Folder folder);
+ Task MoveSubvaultAsync(Cipher cipher, IEnumerable subvaultIds, Guid userId);
Task ImportCiphersAsync(List folders, List ciphers,
IEnumerable> folderRelationships);
}
diff --git a/src/Core/Services/Implementations/CipherService.cs b/src/Core/Services/Implementations/CipherService.cs
index c910f00932..fe64902bfb 100644
--- a/src/Core/Services/Implementations/CipherService.cs
+++ b/src/Core/Services/Implementations/CipherService.cs
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
using Core.Models.Data;
+using Bit.Core.Exceptions;
namespace Bit.Core.Services
{
@@ -13,17 +14,26 @@ namespace Bit.Core.Services
private readonly ICipherRepository _cipherRepository;
private readonly IFolderRepository _folderRepository;
private readonly IUserRepository _userRepository;
+ private readonly IOrganizationRepository _organizationRepository;
+ private readonly IOrganizationUserRepository _organizationUserRepository;
+ private readonly ISubvaultUserRepository _subvaultUserRepository;
private readonly IPushService _pushService;
public CipherService(
ICipherRepository cipherRepository,
IFolderRepository folderRepository,
IUserRepository userRepository,
+ IOrganizationRepository organizationRepository,
+ IOrganizationUserRepository organizationUserRepository,
+ ISubvaultUserRepository subvaultUserRepository,
IPushService pushService)
{
_cipherRepository = cipherRepository;
_folderRepository = folderRepository;
_userRepository = userRepository;
+ _organizationRepository = organizationRepository;
+ _organizationUserRepository = organizationUserRepository;
+ _subvaultUserRepository = subvaultUserRepository;
_pushService = pushService;
}
@@ -81,6 +91,35 @@ namespace Bit.Core.Services
//await _pushService.PushSyncCipherDeleteAsync(cipher);
}
+ public async Task MoveSubvaultAsync(Cipher cipher, IEnumerable subvaultIds, Guid userId)
+ {
+ if(cipher.Id == default(Guid))
+ {
+ throw new BadRequestException(nameof(cipher.Id));
+ }
+
+ if(!cipher.OrganizationId.HasValue)
+ {
+ throw new BadRequestException(nameof(cipher.OrganizationId));
+ }
+
+ var existingCipher = await _cipherRepository.GetByIdAsync(cipher.Id);
+ if(existingCipher == null || (existingCipher.UserId.HasValue && existingCipher.UserId != userId))
+ {
+ throw new NotFoundException();
+ }
+
+ var subvaultUserDetails = await _subvaultUserRepository.GetPermissionsByUserIdAsync(userId, subvaultIds,
+ cipher.OrganizationId.Value);
+
+ cipher.UserId = null;
+ cipher.RevisionDate = DateTime.UtcNow;
+ await _cipherRepository.ReplaceAsync(cipher, subvaultUserDetails.Where(s => s.Admin).Select(s => s.SubvaultId));
+
+ // push
+ await _pushService.PushSyncCipherUpdateAsync(cipher);
+ }
+
public async Task ImportCiphersAsync(
List folders,
List ciphers,
diff --git a/src/Core/Services/Implementations/PushSharpPushService.cs b/src/Core/Services/Implementations/PushSharpPushService.cs
index de39fbf8ca..b3815e66f3 100644
--- a/src/Core/Services/Implementations/PushSharpPushService.cs
+++ b/src/Core/Services/Implementations/PushSharpPushService.cs
@@ -71,7 +71,7 @@ namespace Bit.Core.Services
{
Type = type,
Id = cipher.Id,
- UserId = cipher.UserId,
+ UserId = cipher.UserId.Value,
RevisionDate = cipher.RevisionDate,
Aps = new PushNotification.AppleData { ContentAvailable = 1 }
};
@@ -82,7 +82,7 @@ namespace Bit.Core.Services
excludedTokens.Add(_currentContext.DeviceIdentifier);
}
- await PushToAllUserDevicesAsync(cipher.UserId, JObject.FromObject(message), excludedTokens);
+ await PushToAllUserDevicesAsync(cipher.UserId.Value, JObject.FromObject(message), excludedTokens);
}
public async Task PushSyncCiphersAsync(Guid userId)
diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs
index 56bed884f0..e411041403 100644
--- a/src/Core/Utilities/CoreHelpers.cs
+++ b/src/Core/Utilities/CoreHelpers.cs
@@ -1,4 +1,7 @@
-using System;
+using Dapper;
+using System;
+using System.Collections.Generic;
+using System.Data;
using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;
@@ -40,6 +43,28 @@ namespace Bit.Core.Utilities
return new Guid(guidArray);
}
+ public static DataTable ToGuidIdArrayTVP(this IEnumerable ids)
+ {
+ return ids.ToArrayTVP("GuidId");
+ }
+
+ public static DataTable ToArrayTVP(this IEnumerable values, string columnName)
+ {
+ var table = new DataTable();
+ table.Columns.Add(columnName, typeof(T));
+ table.SetTypeName($"[dbo].[{columnName}Array]");
+
+ if(values != null)
+ {
+ foreach(var value in values)
+ {
+ table.Rows.Add(value);
+ }
+ }
+
+ return table;
+ }
+
public static X509Certificate2 GetCertificate(string thumbprint)
{
// Clean possible garbage characters from thumbprint copy/paste
diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj
index 44f6e567ca..adf7ad4678 100644
--- a/src/Sql/Sql.sqlproj
+++ b/src/Sql/Sql.sqlproj
@@ -63,6 +63,7 @@
+
@@ -168,5 +169,8 @@
+
+
+
\ No newline at end of file
diff --git a/src/Sql/dbo/Stored Procedures/CipherDetails_ReadByUserId.sql b/src/Sql/dbo/Stored Procedures/CipherDetails_ReadByUserId.sql
index 5b6a1e1e89..74c2a44cbb 100644
--- a/src/Sql/dbo/Stored Procedures/CipherDetails_ReadByUserId.sql
+++ b/src/Sql/dbo/Stored Procedures/CipherDetails_ReadByUserId.sql
@@ -4,10 +4,17 @@ AS
BEGIN
SET NOCOUNT ON
- SELECT
- *
+ SELECT DISTINCT
+ C.*
FROM
- [dbo].[CipherDetailsView]
+ [dbo].[CipherDetailsView] C
+ LEFT JOIN
+ [dbo].[SubvaultCipher] SC ON SC.[CipherId] = C.[Id]
+ LEFT JOIN
+ [dbo].[SubvaultUser] SU ON SU.[SubvaultId] = SC.[SubvaultId]
+ LEFT JOIN
+ [dbo].[OrganizationUser] OU ON OU.[Id] = SU.[OrganizationUserId]
WHERE
- [UserId] = @UserId
+ (C.[UserId] IS NOT NULL AND C.[UserId] = @UserId)
+ OR OU.[UserId] = @UserId
END
\ No newline at end of file
diff --git a/src/Sql/dbo/Stored Procedures/Cipher_UpdateWithSubvaults.sql b/src/Sql/dbo/Stored Procedures/Cipher_UpdateWithSubvaults.sql
new file mode 100644
index 0000000000..811c9239ac
--- /dev/null
+++ b/src/Sql/dbo/Stored Procedures/Cipher_UpdateWithSubvaults.sql
@@ -0,0 +1,43 @@
+CREATE PROCEDURE [dbo].[Cipher_UpdateWithSubvaults]
+ @Id UNIQUEIDENTIFIER,
+ @UserId UNIQUEIDENTIFIER,
+ @OrganizationId UNIQUEIDENTIFIER,
+ @Type TINYINT,
+ @Data NVARCHAR(MAX),
+ @CreationDate DATETIME2(7),
+ @RevisionDate DATETIME2(7),
+ @SubvaultIds AS [dbo].[GuidIdArray] READONLY
+AS
+BEGIN
+ SET NOCOUNT ON
+
+ UPDATE
+ [dbo].[Cipher]
+ SET
+ [UserId] = @UserId,
+ [OrganizationId] = @OrganizationId,
+ [Type] = @Type,
+ [Data] = @Data,
+ [CreationDate] = @CreationDate,
+ [RevisionDate] = @RevisionDate
+ WHERE
+ [Id] = @Id
+
+ MERGE
+ [dbo].[SubvaultCipher] AS [Target]
+ USING
+ @SubvaultIds AS [Source]
+ ON
+ [Target].[SubvaultId] = [Source].[Id]
+ AND [Target].[CipherId] = @Id
+ WHEN NOT MATCHED BY TARGET THEN
+ INSERT VALUES
+ (
+ [Source].[Id],
+ @Id
+ )
+ WHEN NOT MATCHED BY SOURCE
+ AND [Target].[CipherId] = @Id THEN
+ DELETE
+ ;
+END
\ No newline at end of file
diff --git a/src/Sql/dbo/Stored Procedures/SubvaultUser_ReadPermissionsBySubvaultUserId.sql b/src/Sql/dbo/Stored Procedures/SubvaultUser_ReadPermissionsBySubvaultUserId.sql
new file mode 100644
index 0000000000..3acb89b26a
--- /dev/null
+++ b/src/Sql/dbo/Stored Procedures/SubvaultUser_ReadPermissionsBySubvaultUserId.sql
@@ -0,0 +1,22 @@
+CREATE PROCEDURE [dbo].[SubvaultUser_ReadPermissionsBySubvaultUserId]
+ @UserId UNIQUEIDENTIFIER,
+ @SubvaultIds AS [dbo].[GuidIdArray] READONLY,
+ @OrganizationId UNIQUEIDENTIFIER
+AS
+BEGIN
+ SET NOCOUNT ON
+
+ SELECT
+ SU.[SubvaultId],
+ CASE WHEN OU.[Type] = 2 THEN SU.[Admin] ELSE 1 END AS [Admin], -- 2 = Regular User
+ CASE WHEN OU.[Type] = 2 THEN SU.[ReadOnly] ELSE 0 END AS [ReadOnly] -- 2 = Regular User
+ FROM
+ [dbo].[SubvaultUser] SU
+ INNER JOIN
+ [dbo].[OrganizationUser] OU ON OU.Id = SU.OrganizationUserId
+ WHERE
+ OU.[UserId] = @UserId
+ AND OU.[OrganizationId] = @OrganizationId
+ AND OU.[Status] = 2 -- 2 = Confirmed
+ AND SU.[SubvaultId] IN (SELECT [Id] FROM @SubvaultIds)
+END
\ No newline at end of file
diff --git a/src/Sql/dbo/Tables/History.sql b/src/Sql/dbo/Tables/History.sql
index e86b6d30d2..ec68d90ca0 100644
--- a/src/Sql/dbo/Tables/History.sql
+++ b/src/Sql/dbo/Tables/History.sql
@@ -1,6 +1,6 @@
CREATE TABLE [dbo].[History] (
[Id] BIGINT IDENTITY (1, 1) NOT NULL,
- [UserId] UNIQUEIDENTIFIER NOT NULL,
+ [UserId] UNIQUEIDENTIFIER NULL,
[CipherId] UNIQUEIDENTIFIER NOT NULL,
[Event] TINYINT NOT NULL,
[Date] DATETIME2 (7) NOT NULL,
diff --git a/src/Sql/dbo/UserDefinedTypes/GuidIdArray.sql b/src/Sql/dbo/UserDefinedTypes/GuidIdArray.sql
new file mode 100644
index 0000000000..4d3f49e4e4
--- /dev/null
+++ b/src/Sql/dbo/UserDefinedTypes/GuidIdArray.sql
@@ -0,0 +1 @@
+CREATE TYPE [dbo].[GuidIdArray] AS TABLE ([Id] UNIQUEIDENTIFIER NOT NULL);