From 2b8db4d1edb6da9c9b85b060469e652e7193a769 Mon Sep 17 00:00:00 2001
From: Kyle Spearrin <kyle.spearrin@gmail.com>
Date: Thu, 11 May 2017 11:41:13 -0400
Subject: [PATCH] SelectionReadOnly MERGE to CollectionGroup

---
 src/Api/Controllers/GroupsController.cs       |  4 +--
 .../Models/Api/Request/GroupRequestModel.cs   |  2 +-
 .../OrganizationUserRequestModels.cs          | 23 ++-----------
 .../Request/SelectionReadOnlyRequestModel.cs  | 33 +++++++++++++++++++
 .../Models/Api/Response/GroupResponseModel.cs |  8 +++--
 .../Response/OrganizationUserResponseModel.cs | 21 ++----------
 .../SelectionReadOnlyResponseModel.cs         | 22 +++++++++++++
 src/Core/Models/Table/CollectionGroup.cs      |  2 +-
 src/Core/Repositories/IGroupRepository.cs     |  6 ++--
 .../Repositories/SqlServer/GroupRepository.cs | 16 ++++-----
 src/Core/Services/IGroupService.cs            |  4 +--
 .../Services/Implementations/GroupService.cs  | 11 ++++---
 src/Core/Utilities/CoreHelpers.cs             | 28 +++++++++++++++-
 src/Sql/Sql.sqlproj                           |  1 +
 .../Group_CreateWithCollections.sql           |  6 ++--
 .../Group_ReadWithCollectionsById.sql         |  3 +-
 .../Group_UpdateWithCollections.sql           |  8 +++--
 .../SelectionReadOnlyArray.sql                |  4 +++
 18 files changed, 129 insertions(+), 73 deletions(-)
 create mode 100644 src/Core/Models/Api/Request/SelectionReadOnlyRequestModel.cs
 create mode 100644 src/Core/Models/Api/Response/SelectionReadOnlyResponseModel.cs
 create mode 100644 src/Sql/dbo/User Defined Types/SelectionReadOnlyArray.sql

diff --git a/src/Api/Controllers/GroupsController.cs b/src/Api/Controllers/GroupsController.cs
index 8a8feffca6..e9eea63a51 100644
--- a/src/Api/Controllers/GroupsController.cs
+++ b/src/Api/Controllers/GroupsController.cs
@@ -93,7 +93,7 @@ namespace Bit.Api.Controllers
             }
 
             var group = model.ToGroup(orgIdGuid);
-            await _groupService.SaveAsync(group, model.CollectionIds?.Select(c => new Guid(c)));
+            await _groupService.SaveAsync(group, model.Collections?.Select(c => c.ToSelectionReadOnly()));
             return new GroupResponseModel(group);
         }
 
@@ -107,7 +107,7 @@ namespace Bit.Api.Controllers
                 throw new NotFoundException();
             }
 
-            await _groupService.SaveAsync(model.ToGroup(group), model.CollectionIds?.Select(c => new Guid(c)));
+            await _groupService.SaveAsync(model.ToGroup(group), model.Collections?.Select(c => c.ToSelectionReadOnly()));
             return new GroupResponseModel(group);
         }
 
diff --git a/src/Core/Models/Api/Request/GroupRequestModel.cs b/src/Core/Models/Api/Request/GroupRequestModel.cs
index 6125e32b20..d5366f8d3e 100644
--- a/src/Core/Models/Api/Request/GroupRequestModel.cs
+++ b/src/Core/Models/Api/Request/GroupRequestModel.cs
@@ -13,7 +13,7 @@ namespace Bit.Core.Models.Api
         public string Name { get; set; }
         [Required]
         public bool? AccessAll { get; set; }
-        public IEnumerable<string> CollectionIds { get; set; }
+        public IEnumerable<SelectionReadOnlyRequestModel> Collections { get; set; }
 
         public Group ToGroup(Guid orgId)
         {
diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs b/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs
index 6fa68d38f9..e694a4fcce 100644
--- a/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs
+++ b/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs
@@ -1,5 +1,4 @@
 using Bit.Core.Models.Table;
-using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 
@@ -13,7 +12,7 @@ namespace Bit.Core.Models.Api
         [Required]
         public Enums.OrganizationUserType? Type { get; set; }
         public bool AccessAll { get; set; }
-        public IEnumerable<OrganizationUserCollectionRequestModel> Collections { get; set; }
+        public IEnumerable<SelectionReadOnlyRequestModel> Collections { get; set; }
     }
 
     public class OrganizationUserAcceptRequestModel
@@ -33,7 +32,7 @@ namespace Bit.Core.Models.Api
         [Required]
         public Enums.OrganizationUserType? Type { get; set; }
         public bool AccessAll { get; set; }
-        public IEnumerable<OrganizationUserCollectionRequestModel> Collections { get; set; }
+        public IEnumerable<SelectionReadOnlyRequestModel> Collections { get; set; }
 
         public OrganizationUser ToOrganizationUser(OrganizationUser existingUser)
         {
@@ -48,22 +47,4 @@ namespace Bit.Core.Models.Api
         [Required]
         public IEnumerable<string> GroupIds { get; set; }
     }
-
-    public class OrganizationUserCollectionRequestModel
-    {
-        [Required]
-        public string CollectionId { get; set; }
-        public bool ReadOnly { get; set; }
-
-        public CollectionUser ToCollectionUser()
-        {
-            var collection = new CollectionUser
-            {
-                ReadOnly = ReadOnly,
-                CollectionId = new Guid(CollectionId)
-            };
-
-            return collection;
-        }
-    }
 }
diff --git a/src/Core/Models/Api/Request/SelectionReadOnlyRequestModel.cs b/src/Core/Models/Api/Request/SelectionReadOnlyRequestModel.cs
new file mode 100644
index 0000000000..9833946e60
--- /dev/null
+++ b/src/Core/Models/Api/Request/SelectionReadOnlyRequestModel.cs
@@ -0,0 +1,33 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using Newtonsoft.Json;
+using Bit.Core.Models.Table;
+using Bit.Core.Models.Data;
+
+namespace Bit.Core.Models.Api
+{
+    public class SelectionReadOnlyRequestModel
+    {
+        [Required]
+        public string Id { get; set; }
+        public bool ReadOnly { get; set; }
+
+        public CollectionUser ToCollectionUser()
+        {
+            return new CollectionUser
+            {
+                ReadOnly = ReadOnly,
+                CollectionId = new Guid(Id)
+            };
+        }
+
+        public SelectionReadOnly ToSelectionReadOnly()
+        {
+            return new SelectionReadOnly
+            {
+                Id = new Guid(Id),
+                ReadOnly = ReadOnly
+            };
+        }
+    }
+}
diff --git a/src/Core/Models/Api/Response/GroupResponseModel.cs b/src/Core/Models/Api/Response/GroupResponseModel.cs
index 1b4cabd057..39d4dc8538 100644
--- a/src/Core/Models/Api/Response/GroupResponseModel.cs
+++ b/src/Core/Models/Api/Response/GroupResponseModel.cs
@@ -1,6 +1,8 @@
 using System;
 using Bit.Core.Models.Table;
 using System.Collections.Generic;
+using Bit.Core.Models.Data;
+using System.Linq;
 
 namespace Bit.Core.Models.Api
 {
@@ -28,12 +30,12 @@ namespace Bit.Core.Models.Api
 
     public class GroupDetailsResponseModel : GroupResponseModel
     {
-        public GroupDetailsResponseModel(Group group, IEnumerable<Guid> collectionIds)
+        public GroupDetailsResponseModel(Group group, IEnumerable<SelectionReadOnly> collections)
             : base(group, "groupDetails")
         {
-            CollectionIds = collectionIds;
+            Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c));
         }
 
-        public IEnumerable<Guid> CollectionIds { get; set; }
+        public IEnumerable<SelectionReadOnlyResponseModel> Collections { get; set; }
     }
 }
diff --git a/src/Core/Models/Api/Response/OrganizationUserResponseModel.cs b/src/Core/Models/Api/Response/OrganizationUserResponseModel.cs
index 0b1ef2612e..43c80fcfa6 100644
--- a/src/Core/Models/Api/Response/OrganizationUserResponseModel.cs
+++ b/src/Core/Models/Api/Response/OrganizationUserResponseModel.cs
@@ -52,27 +52,10 @@ namespace Bit.Core.Models.Api
             IEnumerable<SelectionReadOnly> collections)
             : base(organizationUser, "organizationUserDetails")
         {
-            Collections = collections.Select(c => new CollectionSelection(c));
+            Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c));
         }
 
-        public IEnumerable<CollectionSelection> Collections { get; set; }
-
-        public class CollectionSelection
-        {
-            public CollectionSelection(Data.SelectionReadOnly selection)
-            {
-                if(selection == null)
-                {
-                    throw new ArgumentNullException(nameof(selection));
-                }
-
-                Id = selection.Id.ToString();
-                ReadOnly = selection.ReadOnly;
-            }
-
-            public string Id { get; set; }
-            public bool ReadOnly { get; set; }
-        }
+        public IEnumerable<SelectionReadOnlyResponseModel> Collections { get; set; }
     }
     public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponseModel
     {
diff --git a/src/Core/Models/Api/Response/SelectionReadOnlyResponseModel.cs b/src/Core/Models/Api/Response/SelectionReadOnlyResponseModel.cs
new file mode 100644
index 0000000000..dce5ffd456
--- /dev/null
+++ b/src/Core/Models/Api/Response/SelectionReadOnlyResponseModel.cs
@@ -0,0 +1,22 @@
+using System;
+using Bit.Core.Models.Data;
+
+namespace Bit.Core.Models.Api
+{
+    public class SelectionReadOnlyResponseModel
+    {
+        public SelectionReadOnlyResponseModel(SelectionReadOnly selection)
+        {
+            if(selection == null)
+            {
+                throw new ArgumentNullException(nameof(selection));
+            }
+
+            Id = selection.Id.ToString();
+            ReadOnly = selection.ReadOnly;
+        }
+
+        public string Id { get; set; }
+        public bool ReadOnly { get; set; }
+    }
+}
diff --git a/src/Core/Models/Table/CollectionGroup.cs b/src/Core/Models/Table/CollectionGroup.cs
index 5e6d516a56..d666d73443 100644
--- a/src/Core/Models/Table/CollectionGroup.cs
+++ b/src/Core/Models/Table/CollectionGroup.cs
@@ -5,7 +5,7 @@ namespace Bit.Core.Models.Table
     public class CollectionGroup
     {
         public Guid CollectionId { get; set; }
-        public Guid OrganizationUserId { get; set; }
+        public Guid GroupId { get; set; }
         public bool ReadOnly { get; set; }
     }
 }
diff --git a/src/Core/Repositories/IGroupRepository.cs b/src/Core/Repositories/IGroupRepository.cs
index 53627cb3f3..84b235d34e 100644
--- a/src/Core/Repositories/IGroupRepository.cs
+++ b/src/Core/Repositories/IGroupRepository.cs
@@ -8,12 +8,12 @@ namespace Bit.Core.Repositories
 {
     public interface IGroupRepository : IRepository<Group, Guid>
     {
-        Task<Tuple<Group, ICollection<Guid>>> GetByIdWithCollectionsAsync(Guid id);
+        Task<Tuple<Group, ICollection<SelectionReadOnly>>> GetByIdWithCollectionsAsync(Guid id);
         Task<ICollection<Group>> GetManyByOrganizationIdAsync(Guid organizationId);
         Task<ICollection<GroupUserUserDetails>> GetManyUserDetailsByIdAsync(Guid id);
         Task<ICollection<Guid>> GetManyIdsByUserIdAsync(Guid organizationUserId);
-        Task CreateAsync(Group obj, IEnumerable<Guid> collectionIds);
-        Task ReplaceAsync(Group obj, IEnumerable<Guid> collectionIds);
+        Task CreateAsync(Group obj, IEnumerable<SelectionReadOnly> collections);
+        Task ReplaceAsync(Group obj, IEnumerable<SelectionReadOnly> collections);
         Task DeleteUserAsync(Guid groupId, Guid organizationUserId);
     }
 }
diff --git a/src/Core/Repositories/SqlServer/GroupRepository.cs b/src/Core/Repositories/SqlServer/GroupRepository.cs
index f2013c02c1..2ec55f75fc 100644
--- a/src/Core/Repositories/SqlServer/GroupRepository.cs
+++ b/src/Core/Repositories/SqlServer/GroupRepository.cs
@@ -22,7 +22,7 @@ namespace Bit.Core.Repositories.SqlServer
             : base(connectionString)
         { }
 
-        public async Task<Tuple<Group, ICollection<Guid>>> GetByIdWithCollectionsAsync(Guid id)
+        public async Task<Tuple<Group, ICollection<SelectionReadOnly>>> GetByIdWithCollectionsAsync(Guid id)
         {
             using(var connection = new SqlConnection(ConnectionString))
             {
@@ -32,9 +32,9 @@ namespace Bit.Core.Repositories.SqlServer
                     commandType: CommandType.StoredProcedure);
 
                 var group = await results.ReadFirstOrDefaultAsync<Group>();
-                var colletionIds = (await results.ReadAsync<Guid>()).ToList();
+                var colletions = (await results.ReadAsync<SelectionReadOnly>()).ToList();
 
-                return new Tuple<Group, ICollection<Guid>>(group, colletionIds);
+                return new Tuple<Group, ICollection<SelectionReadOnly>>(group, colletions);
             }
         }
 
@@ -77,11 +77,11 @@ namespace Bit.Core.Repositories.SqlServer
             }
         }
 
-        public async Task CreateAsync(Group obj, IEnumerable<Guid> collectionIds)
+        public async Task CreateAsync(Group obj, IEnumerable<SelectionReadOnly> collections)
         {
             obj.SetNewId();
             var objWithCollections = JsonConvert.DeserializeObject<GroupWithCollections>(JsonConvert.SerializeObject(obj));
-            objWithCollections.CollectionIds = collectionIds.ToGuidIdArrayTVP();
+            objWithCollections.Collections = collections.ToArrayTVP();
 
             using(var connection = new SqlConnection(ConnectionString))
             {
@@ -92,10 +92,10 @@ namespace Bit.Core.Repositories.SqlServer
             }
         }
 
-        public async Task ReplaceAsync(Group obj, IEnumerable<Guid> collectionIds)
+        public async Task ReplaceAsync(Group obj, IEnumerable<SelectionReadOnly> collections)
         {
             var objWithCollections = JsonConvert.DeserializeObject<GroupWithCollections>(JsonConvert.SerializeObject(obj));
-            objWithCollections.CollectionIds = collectionIds.ToGuidIdArrayTVP();
+            objWithCollections.Collections = collections.ToArrayTVP();
 
             using(var connection = new SqlConnection(ConnectionString))
             {
@@ -119,7 +119,7 @@ namespace Bit.Core.Repositories.SqlServer
 
         public class GroupWithCollections : Group
         {
-            public DataTable CollectionIds { get; set; }
+            public DataTable Collections { get; set; }
         }
     }
 }
diff --git a/src/Core/Services/IGroupService.cs b/src/Core/Services/IGroupService.cs
index 303fc36849..602ebc44f5 100644
--- a/src/Core/Services/IGroupService.cs
+++ b/src/Core/Services/IGroupService.cs
@@ -1,12 +1,12 @@
 using System.Threading.Tasks;
 using Bit.Core.Models.Table;
 using System.Collections.Generic;
-using System;
+using Bit.Core.Models.Data;
 
 namespace Bit.Core.Services
 {
     public interface IGroupService
     {
-        Task SaveAsync(Group group, IEnumerable<Guid> collectionIds = null);
+        Task SaveAsync(Group group, IEnumerable<SelectionReadOnly> collections = null);
     }
 }
diff --git a/src/Core/Services/Implementations/GroupService.cs b/src/Core/Services/Implementations/GroupService.cs
index 9e8bc0ba21..2728d88abe 100644
--- a/src/Core/Services/Implementations/GroupService.cs
+++ b/src/Core/Services/Implementations/GroupService.cs
@@ -4,6 +4,7 @@ using Bit.Core.Exceptions;
 using Bit.Core.Models.Table;
 using Bit.Core.Repositories;
 using System.Collections.Generic;
+using Bit.Core.Models.Data;
 
 namespace Bit.Core.Services
 {
@@ -20,7 +21,7 @@ namespace Bit.Core.Services
             _groupRepository = groupRepository;
         }
 
-        public async Task SaveAsync(Group group, IEnumerable<Guid> collectionIds = null)
+        public async Task SaveAsync(Group group, IEnumerable<SelectionReadOnly> collections = null)
         {
             var org = await _organizationRepository.GetByIdAsync(group.OrganizationId);
             if(org == null)
@@ -35,24 +36,24 @@ namespace Bit.Core.Services
 
             if(group.Id == default(Guid))
             {
-                if(collectionIds == null)
+                if(collections == null)
                 {
                     await _groupRepository.CreateAsync(group);
                 }
                 else
                 {
-                    await _groupRepository.CreateAsync(group, collectionIds);
+                    await _groupRepository.CreateAsync(group, collections);
                 }
             }
             else
             {
-                if(collectionIds == null)
+                if(collections == null)
                 {
                     await _groupRepository.ReplaceAsync(group);
                 }
                 else
                 {
-                    await _groupRepository.ReplaceAsync(group, collectionIds);
+                    await _groupRepository.ReplaceAsync(group, collections);
                 }
             }
         }
diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs
index 46c3bafe03..85b482b846 100644
--- a/src/Core/Utilities/CoreHelpers.cs
+++ b/src/Core/Utilities/CoreHelpers.cs
@@ -1,4 +1,6 @@
-using Dapper;
+using Bit.Core.Models.Data;
+using Bit.Core.Models.Table;
+using Dapper;
 using System;
 using System.Collections.Generic;
 using System.Data;
@@ -65,6 +67,30 @@ namespace Bit.Core.Utilities
             return table;
         }
 
+        public static DataTable ToArrayTVP(this IEnumerable<SelectionReadOnly> values)
+        {
+            var table = new DataTable();
+            table.SetTypeName("[dbo].[SelectionReadOnlyArray]");
+
+            var idColumn = new DataColumn("Id", typeof(Guid));
+            table.Columns.Add(idColumn);
+            var readOnlyColumn = new DataColumn("ReadOnly", typeof(bool));
+            table.Columns.Add(readOnlyColumn);
+
+            if(values != null)
+            {
+                foreach(var value in values)
+                {
+                    var row = table.NewRow();
+                    row[idColumn] = value.Id;
+                    row[readOnlyColumn] = value.ReadOnly;
+                    table.Rows.Add(row);
+                }
+            }
+
+            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 011a8da6c3..2c70828afa 100644
--- a/src/Sql/Sql.sqlproj
+++ b/src/Sql/Sql.sqlproj
@@ -193,5 +193,6 @@
     <Build Include="dbo\Stored Procedures\GroupUser_ReadGroupIdsByOrganizationUserId.sql" />
     <Build Include="dbo\Stored Procedures\GroupUser_UpdateGroups.sql" />
     <Build Include="dbo\Stored Procedures\GroupUser_Delete.sql" />
+    <Build Include="dbo\User Defined Types\SelectionReadOnlyArray.sql" />
   </ItemGroup>
 </Project>
\ No newline at end of file
diff --git a/src/Sql/dbo/Stored Procedures/Group_CreateWithCollections.sql b/src/Sql/dbo/Stored Procedures/Group_CreateWithCollections.sql
index 523455604b..be3787c75f 100644
--- a/src/Sql/dbo/Stored Procedures/Group_CreateWithCollections.sql	
+++ b/src/Sql/dbo/Stored Procedures/Group_CreateWithCollections.sql	
@@ -5,7 +5,7 @@
     @AccessAll BIT,
     @CreationDate DATETIME2(7),
     @RevisionDate DATETIME2(7),
-    @CollectionIds AS [dbo].[GuidIdArray] READONLY
+    @Collections AS [dbo].[SelectionReadOnlyArray] READONLY
 AS
 BEGIN
     SET NOCOUNT ON
@@ -29,9 +29,9 @@ BEGIN
     SELECT
         [Id],
         @Id,
-        0
+        [ReadOnly]
     FROM
-        @CollectionIds
+        @Collections
     WHERE
         [Id] IN (SELECT [Id] FROM [AvailableCollectionsCTE])
 END
\ No newline at end of file
diff --git a/src/Sql/dbo/Stored Procedures/Group_ReadWithCollectionsById.sql b/src/Sql/dbo/Stored Procedures/Group_ReadWithCollectionsById.sql
index fb9d2951e4..e06a8cb9aa 100644
--- a/src/Sql/dbo/Stored Procedures/Group_ReadWithCollectionsById.sql	
+++ b/src/Sql/dbo/Stored Procedures/Group_ReadWithCollectionsById.sql	
@@ -7,7 +7,8 @@ BEGIN
     EXEC [dbo].[Group_ReadById] @Id
 
     SELECT
-        [CollectionId]
+        [CollectionId] [Id],
+        [ReadOnly]
     FROM
         [dbo].[CollectionGroup]
     WHERE
diff --git a/src/Sql/dbo/Stored Procedures/Group_UpdateWithCollections.sql b/src/Sql/dbo/Stored Procedures/Group_UpdateWithCollections.sql
index ce16903fa7..549ae193ca 100644
--- a/src/Sql/dbo/Stored Procedures/Group_UpdateWithCollections.sql	
+++ b/src/Sql/dbo/Stored Procedures/Group_UpdateWithCollections.sql	
@@ -5,7 +5,7 @@
     @AccessAll BIT,
     @CreationDate DATETIME2(7),
     @RevisionDate DATETIME2(7),
-    @CollectionIds AS [dbo].[GuidIdArray] READONLY
+    @Collections AS [dbo].[SelectionReadOnlyArray] READONLY
 AS
 BEGIN
     SET NOCOUNT ON
@@ -23,7 +23,7 @@ BEGIN
     MERGE
         [dbo].[CollectionGroup] AS [Target]
     USING 
-        @CollectionIds AS [Source]
+        @Collections AS [Source]
     ON
         [Target].[CollectionId] = [Source].[Id]
         AND [Target].[GroupId] = @Id
@@ -33,8 +33,10 @@ BEGIN
         (
             [Source].[Id],
             @Id,
-            0
+            [Source].[ReadOnly]
         )
+    WHEN MATCHED AND [Target].[ReadOnly] != [Source].[ReadOnly] THEN
+        UPDATE SET [Target].[ReadOnly] = [Source].[ReadOnly]
     WHEN NOT MATCHED BY SOURCE
     AND [Target].[GroupId] = @Id THEN
         DELETE
diff --git a/src/Sql/dbo/User Defined Types/SelectionReadOnlyArray.sql b/src/Sql/dbo/User Defined Types/SelectionReadOnlyArray.sql
new file mode 100644
index 0000000000..456d913e01
--- /dev/null
+++ b/src/Sql/dbo/User Defined Types/SelectionReadOnlyArray.sql	
@@ -0,0 +1,4 @@
+CREATE TYPE [dbo].[SelectionReadOnlyArray] AS TABLE (
+    [Id] UNIQUEIDENTIFIER NOT NULL,
+    [ReadOnly] BIT NOT NULL);
+