1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-22 21:45:15 -05:00

Move into and read ciphers from org subvaults

This commit is contained in:
Kyle Spearrin 2017-03-21 00:04:39 -04:00
parent 4779794599
commit ed8d5d69a4
22 changed files with 283 additions and 16 deletions

View File

@ -88,6 +88,21 @@ namespace Bit.Api.Controllers
// await _cipherService.SaveAsync(cipher); // 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}")] [HttpDelete("{id}")]
[HttpPost("{id}/delete")] [HttpPost("{id}/delete")]
public async Task Delete(string id) public async Task Delete(string id)

View File

@ -19,6 +19,7 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="1.1.2" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="1.1.2" />
<PackageReference Include="Dapper" Version="1.50.2" /> <PackageReference Include="Dapper" Version="1.50.2" />
<PackageReference Include="DataTableProxy" Version="1.2.0" /> <PackageReference Include="DataTableProxy" Version="1.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.1" />
<PackageReference Include="Sendgrid" Version="9.0.12" /> <PackageReference Include="Sendgrid" Version="9.0.12" />
<PackageReference Include="PushSharp" Version="4.0.10" /> <PackageReference Include="PushSharp" Version="4.0.10" />
<PackageReference Include="WindowsAzure.Storage" Version="8.1.1" /> <PackageReference Include="WindowsAzure.Storage" Version="8.1.1" />

View File

@ -4,6 +4,7 @@ using Bit.Core.Utilities;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using Bit.Core.Enums; using Bit.Core.Enums;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Collections.Generic;
namespace Bit.Core.Models.Api namespace Bit.Core.Models.Api
{ {
@ -15,6 +16,8 @@ namespace Bit.Core.Models.Api
[StringLength(36)] [StringLength(36)]
public string Id { get; set; } public string Id { get; set; }
[StringLength(36)] [StringLength(36)]
public string OrganizationId { get; set; }
[StringLength(36)]
public string FolderId { get; set; } public string FolderId { get; set; }
[Required] [Required]
[EncryptedString] [EncryptedString]
@ -35,24 +38,35 @@ namespace Bit.Core.Models.Api
public virtual Cipher ToCipher(Guid userId) public virtual Cipher ToCipher(Guid userId)
{ {
var cipher = new Cipher return ToCipher(new Cipher
{ {
Id = new Guid(Id), Id = new Guid(Id),
UserId = userId, UserId = string.IsNullOrWhiteSpace(OrganizationId) ? (Guid?)userId : null,
//FolderId = string.IsNullOrWhiteSpace(FolderId) ? null : (Guid?)new Guid(FolderId),
Type = Type 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: 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; break;
default: default:
throw new ArgumentException("Unsupported " + nameof(Type) + "."); throw new ArgumentException("Unsupported " + nameof(Type) + ".");
} }
return cipher; return existingCipher;
} }
} }
public class CipherMoveRequestModel
{
public IEnumerable<string> SubvaultIds { get; set; }
[Required]
public CipherRequestModel Cipher { get; set; }
}
} }

View File

@ -1,5 +1,8 @@
using System; using System;
using Core.Models.Data; using Core.Models.Data;
using System.Collections.Generic;
using Bit.Core.Models.Table;
using System.Linq;
namespace Bit.Core.Models.Api namespace Bit.Core.Models.Api
{ {
@ -16,6 +19,7 @@ namespace Bit.Core.Models.Api
Id = cipher.Id.ToString(); Id = cipher.Id.ToString();
Type = cipher.Type; Type = cipher.Type;
RevisionDate = cipher.RevisionDate; RevisionDate = cipher.RevisionDate;
OrganizationId = cipher.OrganizationId?.ToString();
FolderId = cipher.FolderId?.ToString(); FolderId = cipher.FolderId?.ToString();
Favorite = cipher.Favorite; Favorite = cipher.Favorite;
@ -30,10 +34,22 @@ namespace Bit.Core.Models.Api
} }
public string Id { get; set; } public string Id { get; set; }
public string OrganizationId { get; set; }
public string FolderId { get; set; } public string FolderId { get; set; }
public Enums.CipherType Type { get; set; } public Enums.CipherType Type { get; set; }
public bool Favorite { get; set; } public bool Favorite { get; set; }
public dynamic Data { get; set; } public dynamic Data { get; set; }
public DateTime RevisionDate { get; set; } public DateTime RevisionDate { get; set; }
} }
public class CipherDetailsResponseModel : CipherResponseModel
{
public CipherDetailsResponseModel(CipherDetails cipher, IEnumerable<SubvaultCipher> subvaultCipher)
: base(cipher, "cipherDetails")
{
SubvaultIds = subvaultCipher.Select(s => s.SubvaultId);
}
public IEnumerable<Guid> SubvaultIds { get; set; }
}
} }

View File

@ -21,6 +21,7 @@ namespace Bit.Core.Models.Api
var data = new LoginDataModel(cipher); var data = new LoginDataModel(cipher);
Id = cipher.Id.ToString(); Id = cipher.Id.ToString();
OrganizationId = cipher.OrganizationId?.ToString();
FolderId = cipher.FolderId?.ToString(); FolderId = cipher.FolderId?.ToString();
Favorite = cipher.Favorite; Favorite = cipher.Favorite;
Name = data.Name; Name = data.Name;
@ -32,6 +33,7 @@ namespace Bit.Core.Models.Api
} }
public string Id { get; set; } public string Id { get; set; }
public string OrganizationId { get; set; }
public string FolderId { get; set; } public string FolderId { get; set; }
public bool Favorite { get; set; } public bool Favorite { get; set; }
public string Name { get; set; } public string Name { get; set; }

View File

@ -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; }
}
}

View File

@ -6,7 +6,7 @@ namespace Bit.Core.Models.Table
public class Cipher : IDataObject<Guid> public class Cipher : IDataObject<Guid>
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid UserId { get; set; } public Guid? UserId { get; set; }
public Guid? OrganizationId { get; set; } public Guid? OrganizationId { get; set; }
public Enums.CipherType Type { get; set; } public Enums.CipherType Type { get; set; }
public string Data { get; set; } public string Data { get; set; }

View File

@ -0,0 +1,10 @@
using System;
namespace Bit.Core.Models.Table
{
public class SubvaultCipher
{
public Guid SubvaultId { get; set; }
public Guid CipherId { get; set; }
}
}

View File

@ -16,6 +16,7 @@ namespace Bit.Core.Repositories
Task CreateAsync(CipherDetails cipher); Task CreateAsync(CipherDetails cipher);
Task ReplaceAsync(CipherDetails cipher); Task ReplaceAsync(CipherDetails cipher);
Task UpsertAsync(CipherDetails cipher); Task UpsertAsync(CipherDetails cipher);
Task ReplaceAsync(Cipher obj, IEnumerable<Guid> subvaultIds);
Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable<Cipher> ciphers); Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable<Cipher> ciphers);
Task CreateAsync(IEnumerable<Cipher> ciphers); Task CreateAsync(IEnumerable<Cipher> ciphers);
} }

View File

@ -2,11 +2,15 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using System.Collections.Generic; using System.Collections.Generic;
using Bit.Core.Models.Data;
namespace Bit.Core.Repositories namespace Bit.Core.Repositories
{ {
public interface ISubvaultUserRepository : IRepository<SubvaultUser, Guid> public interface ISubvaultUserRepository : IRepository<SubvaultUser, Guid>
{ {
Task<ICollection<SubvaultUser>> GetManyByOrganizationUserIdAsync(Guid orgUserId); Task<ICollection<SubvaultUser>> GetManyByOrganizationUserIdAsync(Guid orgUserId);
Task<ICollection<SubvaultUserDetails>> GetManyDetailsByUserIdAsync(Guid userId);
Task<ICollection<SubvaultUserPermissions>> GetPermissionsByUserIdAsync(Guid userId, IEnumerable<Guid> subvaultIds,
Guid organizationId);
} }
} }

View File

@ -8,6 +8,8 @@ using Bit.Core.Models.Table;
using System.Data; using System.Data;
using Dapper; using Dapper;
using Core.Models.Data; using Core.Models.Data;
using Bit.Core.Utilities;
using Newtonsoft.Json;
namespace Bit.Core.Repositories.SqlServer namespace Bit.Core.Repositories.SqlServer
{ {
@ -120,6 +122,20 @@ namespace Bit.Core.Repositories.SqlServer
} }
} }
public async Task ReplaceAsync(Cipher obj, IEnumerable<Guid> subvaultIds)
{
var objWithSubvaults = JsonConvert.DeserializeObject<CipherWithSubvaults>(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<Cipher> ciphers) public Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable<Cipher> ciphers)
{ {
if(ciphers.Count() == 0) if(ciphers.Count() == 0)
@ -252,5 +268,10 @@ namespace Bit.Core.Repositories.SqlServer
return Task.FromResult(0); return Task.FromResult(0);
} }
public class CipherWithSubvaults : Cipher
{
public DataTable SubvaultIds { get; set; }
}
} }
} }

View File

@ -6,6 +6,8 @@ using System.Data;
using System.Data.SqlClient; using System.Data.SqlClient;
using Dapper; using Dapper;
using System.Linq; using System.Linq;
using Bit.Core.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Core.Repositories.SqlServer namespace Bit.Core.Repositories.SqlServer
{ {
@ -31,5 +33,32 @@ namespace Bit.Core.Repositories.SqlServer
return results.ToList(); return results.ToList();
} }
} }
public async Task<ICollection<SubvaultUserDetails>> GetManyDetailsByUserIdAsync(Guid userId)
{
using(var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<SubvaultUserDetails>(
$"[{Schema}].[SubvaultUserDetails_ReadByUserId]",
new { UserId = userId },
commandType: CommandType.StoredProcedure);
return results.ToList();
}
}
public async Task<ICollection<SubvaultUserPermissions>> GetPermissionsByUserIdAsync(Guid userId,
IEnumerable<Guid> subvaultIds, Guid organizationId)
{
using(var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<SubvaultUserPermissions>(
$"[{Schema}].[SubvaultUser_ReadPermissionsBySubvaultUserId]",
new { UserId = userId, SubvaultIds = subvaultIds.ToGuidIdArrayTVP(), OrganizationId = organizationId },
commandType: CommandType.StoredProcedure);
return results.ToList();
}
}
} }
} }

View File

@ -2,6 +2,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using Core.Models.Data; using Core.Models.Data;
using System;
namespace Bit.Core.Services namespace Bit.Core.Services
{ {
@ -11,6 +12,7 @@ namespace Bit.Core.Services
Task DeleteAsync(Cipher cipher); Task DeleteAsync(Cipher cipher);
Task SaveFolderAsync(Folder folder); Task SaveFolderAsync(Folder folder);
Task DeleteFolderAsync(Folder folder); Task DeleteFolderAsync(Folder folder);
Task MoveSubvaultAsync(Cipher cipher, IEnumerable<Guid> subvaultIds, Guid userId);
Task ImportCiphersAsync(List<Folder> folders, List<CipherDetails> ciphers, Task ImportCiphersAsync(List<Folder> folders, List<CipherDetails> ciphers,
IEnumerable<KeyValuePair<int, int>> folderRelationships); IEnumerable<KeyValuePair<int, int>> folderRelationships);
} }

View File

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Core.Models.Data; using Core.Models.Data;
using Bit.Core.Exceptions;
namespace Bit.Core.Services namespace Bit.Core.Services
{ {
@ -13,17 +14,26 @@ namespace Bit.Core.Services
private readonly ICipherRepository _cipherRepository; private readonly ICipherRepository _cipherRepository;
private readonly IFolderRepository _folderRepository; private readonly IFolderRepository _folderRepository;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly ISubvaultUserRepository _subvaultUserRepository;
private readonly IPushService _pushService; private readonly IPushService _pushService;
public CipherService( public CipherService(
ICipherRepository cipherRepository, ICipherRepository cipherRepository,
IFolderRepository folderRepository, IFolderRepository folderRepository,
IUserRepository userRepository, IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
ISubvaultUserRepository subvaultUserRepository,
IPushService pushService) IPushService pushService)
{ {
_cipherRepository = cipherRepository; _cipherRepository = cipherRepository;
_folderRepository = folderRepository; _folderRepository = folderRepository;
_userRepository = userRepository; _userRepository = userRepository;
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
_subvaultUserRepository = subvaultUserRepository;
_pushService = pushService; _pushService = pushService;
} }
@ -81,6 +91,35 @@ namespace Bit.Core.Services
//await _pushService.PushSyncCipherDeleteAsync(cipher); //await _pushService.PushSyncCipherDeleteAsync(cipher);
} }
public async Task MoveSubvaultAsync(Cipher cipher, IEnumerable<Guid> 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( public async Task ImportCiphersAsync(
List<Folder> folders, List<Folder> folders,
List<CipherDetails> ciphers, List<CipherDetails> ciphers,

View File

@ -71,7 +71,7 @@ namespace Bit.Core.Services
{ {
Type = type, Type = type,
Id = cipher.Id, Id = cipher.Id,
UserId = cipher.UserId, UserId = cipher.UserId.Value,
RevisionDate = cipher.RevisionDate, RevisionDate = cipher.RevisionDate,
Aps = new PushNotification.AppleData { ContentAvailable = 1 } Aps = new PushNotification.AppleData { ContentAvailable = 1 }
}; };
@ -82,7 +82,7 @@ namespace Bit.Core.Services
excludedTokens.Add(_currentContext.DeviceIdentifier); 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) public async Task PushSyncCiphersAsync(Guid userId)

View File

@ -1,4 +1,7 @@
using System; using Dapper;
using System;
using System.Collections.Generic;
using System.Data;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -40,6 +43,28 @@ namespace Bit.Core.Utilities
return new Guid(guidArray); return new Guid(guidArray);
} }
public static DataTable ToGuidIdArrayTVP(this IEnumerable<Guid> ids)
{
return ids.ToArrayTVP("GuidId");
}
public static DataTable ToArrayTVP<T>(this IEnumerable<T> 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) public static X509Certificate2 GetCertificate(string thumbprint)
{ {
// Clean possible garbage characters from thumbprint copy/paste // Clean possible garbage characters from thumbprint copy/paste

View File

@ -63,6 +63,7 @@
<Folder Include="dbo\Tables\" /> <Folder Include="dbo\Tables\" />
<Folder Include="dbo\Views\" /> <Folder Include="dbo\Views\" />
<Folder Include="dbo\Stored Procedures\" /> <Folder Include="dbo\Stored Procedures\" />
<Folder Include="dbo\UserDefinedTypes" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Build Include="dbo\Tables\SubvaultCipher.sql" /> <Build Include="dbo\Tables\SubvaultCipher.sql" />
@ -168,5 +169,8 @@
<Build Include="dbo\Stored Procedures\CipherDetails_Update.sql" /> <Build Include="dbo\Stored Procedures\CipherDetails_Update.sql" />
<Build Include="dbo\Stored Procedures\CipherDetails_Create.sql" /> <Build Include="dbo\Stored Procedures\CipherDetails_Create.sql" />
<Build Include="dbo\Stored Procedures\FolderCipher_DeleteByUserId.sql" /> <Build Include="dbo\Stored Procedures\FolderCipher_DeleteByUserId.sql" />
<Build Include="dbo\Stored Procedures\SubvaultUser_ReadPermissionsBySubvaultUserId.sql" />
<Build Include="dbo\UserDefinedTypes\GuidIdArray.sql" />
<Build Include="dbo\Stored Procedures\Cipher_UpdateWithSubvaults.sql" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -4,10 +4,17 @@ AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON
SELECT SELECT DISTINCT
* C.*
FROM 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 WHERE
[UserId] = @UserId (C.[UserId] IS NOT NULL AND C.[UserId] = @UserId)
OR OU.[UserId] = @UserId
END END

View File

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

View File

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

View File

@ -1,6 +1,6 @@
CREATE TABLE [dbo].[History] ( CREATE TABLE [dbo].[History] (
[Id] BIGINT IDENTITY (1, 1) NOT NULL, [Id] BIGINT IDENTITY (1, 1) NOT NULL,
[UserId] UNIQUEIDENTIFIER NOT NULL, [UserId] UNIQUEIDENTIFIER NULL,
[CipherId] UNIQUEIDENTIFIER NOT NULL, [CipherId] UNIQUEIDENTIFIER NOT NULL,
[Event] TINYINT NOT NULL, [Event] TINYINT NOT NULL,
[Date] DATETIME2 (7) NOT NULL, [Date] DATETIME2 (7) NOT NULL,

View File

@ -0,0 +1 @@
CREATE TYPE [dbo].[GuidIdArray] AS TABLE ([Id] UNIQUEIDENTIFIER NOT NULL);