From 7db36e00050fd74f383d95a2fea118ce057074a0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 17 Oct 2018 14:58:45 -0400 Subject: [PATCH] api adjustments for manager role and collections --- src/Api/Controllers/CollectionsController.cs | 89 +++++----- src/Api/Controllers/GroupsController.cs | 2 +- .../OrganizationUsersController.cs | 2 +- src/Core/CurrentContext.cs | 17 ++ src/Core/Enums/OrganizationUserType.cs | 3 +- src/Core/IdentityServer/ApiResources.cs | 1 + src/Core/IdentityServer/ProfileService.cs | 6 + .../Repositories/ICollectionRepository.cs | 3 + .../SqlServer/CollectionRepository.cs | 54 +++++- src/Core/Services/ICollectionService.cs | 2 +- .../Implementations/CollectionService.cs | 15 +- src/Sql/Sql.sqlproj | 6 +- .../dbo/Functions/UserCollectionDetails.sql | 38 +++++ .../Collection_ReadByIdUserId.sql | 15 ++ .../Collection_ReadByUserId.sql | 35 +--- .../Collection_ReadWithGroupsByIdUserId.sql | 17 ++ .../Collection_UpdateUsers.sql | 41 +++++ .../DbScripts/2018-10-17_00_ManagerRole.sql | 161 ++++++++++++++++++ util/Setup/Setup.csproj | 1 + 19 files changed, 426 insertions(+), 82 deletions(-) create mode 100644 src/Sql/dbo/Functions/UserCollectionDetails.sql create mode 100644 src/Sql/dbo/Stored Procedures/Collection_ReadByIdUserId.sql create mode 100644 src/Sql/dbo/Stored Procedures/Collection_ReadWithGroupsByIdUserId.sql create mode 100644 src/Sql/dbo/Stored Procedures/Collection_UpdateUsers.sql create mode 100644 util/Setup/DbScripts/2018-10-17_00_ManagerRole.sql diff --git a/src/Api/Controllers/CollectionsController.cs b/src/Api/Controllers/CollectionsController.cs index 265a51fcc1..dad5cf0ba1 100644 --- a/src/Api/Controllers/CollectionsController.cs +++ b/src/Api/Controllers/CollectionsController.cs @@ -8,7 +8,7 @@ using Bit.Core.Models.Api; using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core; -using Bit.Core.Models.Data; +using Bit.Core.Models.Table; namespace Bit.Api.Controllers { @@ -36,25 +36,39 @@ namespace Bit.Api.Controllers [HttpGet("{id}")] public async Task Get(string orgId, string id) { - var collection = await _collectionRepository.GetByIdAsync(new Guid(id)); - if(collection == null || !_currentContext.OrganizationAdmin(collection.OrganizationId)) - { - throw new NotFoundException(); - } - + var collection = await GetCollectionAsync(new Guid(id), new Guid(orgId)); return new CollectionResponseModel(collection); } [HttpGet("{id}/details")] public async Task GetDetails(string orgId, string id) { - var collectionDetails = await _collectionRepository.GetByIdWithGroupsAsync(new Guid(id)); - if(collectionDetails?.Item1 == null || !_currentContext.OrganizationAdmin(collectionDetails.Item1.OrganizationId)) + var orgIdGuid = new Guid(orgId); + if(!_currentContext.OrganizationManager(orgIdGuid)) { throw new NotFoundException(); } - return new CollectionGroupDetailsResponseModel(collectionDetails.Item1, collectionDetails.Item2); + var idGuid = new Guid(id); + if(_currentContext.OrganizationAdmin(orgIdGuid)) + { + var collectionDetails = await _collectionRepository.GetByIdWithGroupsAsync(idGuid); + if(collectionDetails?.Item1 == null || collectionDetails.Item1.OrganizationId != orgIdGuid) + { + throw new NotFoundException(); + } + return new CollectionGroupDetailsResponseModel(collectionDetails.Item1, collectionDetails.Item2); + } + else + { + var collectionDetails = await _collectionRepository.GetByIdWithGroupsAsync(idGuid, + _currentContext.UserId.Value); + if(collectionDetails?.Item1 == null || collectionDetails.Item1.OrganizationId != orgIdGuid) + { + throw new NotFoundException(); + } + return new CollectionGroupDetailsResponseModel(collectionDetails.Item1, collectionDetails.Item2); + } } [HttpGet("")] @@ -72,17 +86,10 @@ namespace Bit.Api.Controllers } [HttpGet("~/collections")] - public async Task> GetUser([FromQuery]bool writeOnly = false) + public async Task> GetUser() { var collections = await _collectionRepository.GetManyByUserIdAsync( _userService.GetProperUserId(User).Value); - - // TODO: Deprecated. writeOnly flag can be removed after v1.21.0 - if(writeOnly) - { - collections = collections.Where(c => !c.ReadOnly).ToList(); - } - var responses = collections.Select(c => new CollectionDetailsResponseModel(c)); return new ListResponseModel(responses); } @@ -90,13 +97,7 @@ namespace Bit.Api.Controllers [HttpGet("{id}/users")] public async Task> GetUsers(string orgId, string id) { - var idGuid = new Guid(id); - var collection = await _collectionRepository.GetByIdAsync(idGuid); - if(collection == null || !_currentContext.OrganizationAdmin(collection.OrganizationId)) - { - throw new NotFoundException(); - } - + var collection = await GetCollectionAsync(new Guid(id), new Guid(orgId)); var collectionUsers = await _collectionRepository.GetManyUserDetailsByIdAsync(collection.OrganizationId, collection.Id); var responses = collectionUsers.Select(c => new CollectionUserResponseModel(c)); @@ -107,13 +108,14 @@ namespace Bit.Api.Controllers public async Task Post(string orgId, [FromBody]CollectionRequestModel model) { var orgIdGuid = new Guid(orgId); - if(!_currentContext.OrganizationAdmin(orgIdGuid)) + if(!_currentContext.OrganizationManager(orgIdGuid)) { throw new NotFoundException(); } var collection = model.ToCollection(orgIdGuid); - await _collectionService.SaveAsync(collection, model.Groups?.Select(g => g.ToSelectionReadOnly())); + await _collectionService.SaveAsync(collection, model.Groups?.Select(g => g.ToSelectionReadOnly()), + !_currentContext.OrganizationAdmin(orgIdGuid) ? _currentContext.UserId : null); return new CollectionResponseModel(collection); } @@ -121,12 +123,7 @@ namespace Bit.Api.Controllers [HttpPost("{id}")] public async Task Put(string orgId, string id, [FromBody]CollectionRequestModel model) { - var collection = await _collectionRepository.GetByIdAsync(new Guid(id)); - if(collection == null || !_currentContext.OrganizationAdmin(collection.OrganizationId)) - { - throw new NotFoundException(); - } - + var collection = await GetCollectionAsync(new Guid(id), new Guid(orgId)); await _collectionService.SaveAsync(model.ToCollection(collection), model.Groups?.Select(g => g.ToSelectionReadOnly())); return new CollectionResponseModel(collection); @@ -136,12 +133,7 @@ namespace Bit.Api.Controllers [HttpPost("{id}/delete")] public async Task Delete(string orgId, string id) { - var collection = await _collectionRepository.GetByIdAsync(new Guid(id)); - if(collection == null || !_currentContext.OrganizationAdmin(collection.OrganizationId)) - { - throw new NotFoundException(); - } - + var collection = await GetCollectionAsync(new Guid(id), new Guid(orgId)); await _collectionService.DeleteAsync(collection); } @@ -149,13 +141,26 @@ namespace Bit.Api.Controllers [HttpPost("{id}/delete-user/{orgUserId}")] public async Task Delete(string orgId, string id, string orgUserId) { - var collection = await _collectionRepository.GetByIdAsync(new Guid(id)); - if(collection == null || !_currentContext.OrganizationAdmin(collection.OrganizationId)) + var collection = await GetCollectionAsync(new Guid(id), new Guid(orgId)); + await _collectionService.DeleteUserAsync(collection, new Guid(orgUserId)); + } + + private async Task GetCollectionAsync(Guid id, Guid orgId) + { + if(!_currentContext.OrganizationManager(orgId)) { throw new NotFoundException(); } - await _collectionService.DeleteUserAsync(collection, new Guid(orgUserId)); + var collection = _currentContext.OrganizationAdmin(orgId) ? + await _collectionRepository.GetByIdAsync(id) : + await _collectionRepository.GetByIdAsync(id, _currentContext.UserId.Value); + if(collection == null || collection.OrganizationId != orgId) + { + throw new NotFoundException(); + } + + return collection; } } } diff --git a/src/Api/Controllers/GroupsController.cs b/src/Api/Controllers/GroupsController.cs index f1ee094479..9a9b259223 100644 --- a/src/Api/Controllers/GroupsController.cs +++ b/src/Api/Controllers/GroupsController.cs @@ -57,7 +57,7 @@ namespace Bit.Api.Controllers public async Task> Get(string orgId) { var orgIdGuid = new Guid(orgId); - if(!_currentContext.OrganizationAdmin(orgIdGuid)) + if(!_currentContext.OrganizationManager(orgIdGuid)) { throw new NotFoundException(); } diff --git a/src/Api/Controllers/OrganizationUsersController.cs b/src/Api/Controllers/OrganizationUsersController.cs index d7b240d30c..08b24282d1 100644 --- a/src/Api/Controllers/OrganizationUsersController.cs +++ b/src/Api/Controllers/OrganizationUsersController.cs @@ -58,7 +58,7 @@ namespace Bit.Api.Controllers public async Task> Get(string orgId) { var orgGuidId = new Guid(orgId); - if(!_currentContext.OrganizationAdmin(orgGuidId)) + if(!_currentContext.OrganizationManager(orgGuidId)) { throw new NotFoundException(); } diff --git a/src/Core/CurrentContext.cs b/src/Core/CurrentContext.cs index a344f1e2a4..ebf8a22a8d 100644 --- a/src/Core/CurrentContext.cs +++ b/src/Core/CurrentContext.cs @@ -111,6 +111,16 @@ namespace Bit.Core Type = OrganizationUserType.User })); } + + if(claimsDict.ContainsKey("orgmanager")) + { + Organizations.AddRange(claimsDict["orgmanager"].Select(c => + new CurrentContentOrganization + { + Id = new Guid(c.Value), + Type = OrganizationUserType.Manager + })); + } } public bool OrganizationUser(Guid orgId) @@ -118,6 +128,13 @@ namespace Bit.Core return Organizations?.Any(o => o.Id == orgId) ?? false; } + public bool OrganizationManager(Guid orgId) + { + return Organizations?.Any(o => o.Id == orgId && + (o.Type == OrganizationUserType.Owner || o.Type == OrganizationUserType.Admin || + o.Type == OrganizationUserType.Manager)) ?? false; + } + public bool OrganizationAdmin(Guid orgId) { return Organizations?.Any(o => o.Id == orgId && diff --git a/src/Core/Enums/OrganizationUserType.cs b/src/Core/Enums/OrganizationUserType.cs index 92b1f91855..7021952ef2 100644 --- a/src/Core/Enums/OrganizationUserType.cs +++ b/src/Core/Enums/OrganizationUserType.cs @@ -4,6 +4,7 @@ { Owner = 0, Admin = 1, - User = 2 + User = 2, + Manager = 3, } } diff --git a/src/Core/IdentityServer/ApiResources.cs b/src/Core/IdentityServer/ApiResources.cs index c78b1d7a5d..bdca86b62d 100644 --- a/src/Core/IdentityServer/ApiResources.cs +++ b/src/Core/IdentityServer/ApiResources.cs @@ -19,6 +19,7 @@ namespace Bit.Core.IdentityServer "device", "orgowner", "orgadmin", + "orgmanager", "orguser" }), new ApiResource("internal", new string[] { JwtClaimTypes.Subject }), diff --git a/src/Core/IdentityServer/ProfileService.cs b/src/Core/IdentityServer/ProfileService.cs index e2cf823be1..ad9589cbe0 100644 --- a/src/Core/IdentityServer/ProfileService.cs +++ b/src/Core/IdentityServer/ProfileService.cs @@ -75,6 +75,12 @@ namespace Bit.Core.IdentityServer newClaims.Add(new Claim("orgadmin", org.Id.ToString())); } break; + case Enums.OrganizationUserType.Manager: + foreach(var org in group) + { + newClaims.Add(new Claim("orgmanager", org.Id.ToString())); + } + break; case Enums.OrganizationUserType.User: foreach(var org in group) { diff --git a/src/Core/Repositories/ICollectionRepository.cs b/src/Core/Repositories/ICollectionRepository.cs index 4e00d21803..69f9f21529 100644 --- a/src/Core/Repositories/ICollectionRepository.cs +++ b/src/Core/Repositories/ICollectionRepository.cs @@ -10,11 +10,14 @@ namespace Bit.Core.Repositories { Task GetCountByOrganizationIdAsync(Guid organizationId); Task>> GetByIdWithGroupsAsync(Guid id); + Task>> GetByIdWithGroupsAsync(Guid id, Guid userId); Task> GetManyByOrganizationIdAsync(Guid organizationId); + Task GetByIdAsync(Guid id, Guid userId); Task> GetManyByUserIdAsync(Guid userId); Task> GetManyUserDetailsByIdAsync(Guid organizationId, Guid collectionId); Task CreateAsync(Collection obj, IEnumerable groups); Task ReplaceAsync(Collection obj, IEnumerable groups); Task DeleteUserAsync(Guid collectionId, Guid organizationUserId); + Task UpdateUsersAsync(Guid id, IEnumerable users); } } diff --git a/src/Core/Repositories/SqlServer/CollectionRepository.cs b/src/Core/Repositories/SqlServer/CollectionRepository.cs index 120c0243b2..473bee2154 100644 --- a/src/Core/Repositories/SqlServer/CollectionRepository.cs +++ b/src/Core/Repositories/SqlServer/CollectionRepository.cs @@ -51,6 +51,23 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task>> GetByIdWithGroupsAsync( + Guid id, Guid userId) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryMultipleAsync( + $"[{Schema}].[Collection_ReadWithGroupsByIdUserId]", + new { Id = id, UserId = userId }, + commandType: CommandType.StoredProcedure); + + var collection = await results.ReadFirstOrDefaultAsync(); + var groups = (await results.ReadAsync()).ToList(); + + return new Tuple>(collection, groups); + } + } + public async Task> GetManyByOrganizationIdAsync(Guid organizationId) { using(var connection = new SqlConnection(ConnectionString)) @@ -64,6 +81,19 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task GetByIdAsync(Guid id, Guid userId) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[Collection_ReadByIdUserId]", + new { Id = id, UserId = userId }, + commandType: CommandType.StoredProcedure); + + return results.FirstOrDefault(); + } + } + public async Task> GetManyByUserIdAsync(Guid userId) { using(var connection = new SqlConnection(ConnectionString)) @@ -108,7 +138,7 @@ namespace Bit.Core.Repositories.SqlServer using(var connection = new SqlConnection(ConnectionString)) { var results = await connection.ExecuteAsync( - $"[{Schema}].[Collection_CreateWithGroups]", + $"[{Schema}].[Collection_CreateWithGroupsAndUsers]", objWithGroups, commandType: CommandType.StoredProcedure); } @@ -128,6 +158,17 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task CreateUserAsync(Guid collectionId, Guid organizationUserId) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + $"[{Schema}].[CollectionUser_Create]", + new { CollectionId = collectionId, OrganizationUserId = organizationUserId }, + commandType: CommandType.StoredProcedure); + } + } + public async Task DeleteUserAsync(Guid collectionId, Guid organizationUserId) { using(var connection = new SqlConnection(ConnectionString)) @@ -139,6 +180,17 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task UpdateUsersAsync(Guid id, IEnumerable users) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + $"[{Schema}].[Collection_UpdateUsers]", + new { Id = id, Users = users.ToArrayTVP() }, + commandType: CommandType.StoredProcedure); + } + } + public class CollectionWithGroups : Collection { public DataTable Groups { get; set; } diff --git a/src/Core/Services/ICollectionService.cs b/src/Core/Services/ICollectionService.cs index ae0384f71f..557e2f83fc 100644 --- a/src/Core/Services/ICollectionService.cs +++ b/src/Core/Services/ICollectionService.cs @@ -8,7 +8,7 @@ namespace Bit.Core.Services { public interface ICollectionService { - Task SaveAsync(Collection collection, IEnumerable groups = null); + Task SaveAsync(Collection collection, IEnumerable groups = null, Guid? assignUserId = null); Task DeleteAsync(Collection collection); Task DeleteUserAsync(Collection collection, Guid organizationUserId); } diff --git a/src/Core/Services/Implementations/CollectionService.cs b/src/Core/Services/Implementations/CollectionService.cs index 102b2a1505..3df6c444d8 100644 --- a/src/Core/Services/Implementations/CollectionService.cs +++ b/src/Core/Services/Implementations/CollectionService.cs @@ -33,7 +33,8 @@ namespace Bit.Core.Services _mailService = mailService; } - public async Task SaveAsync(Collection collection, IEnumerable groups = null) + public async Task SaveAsync(Collection collection, IEnumerable groups = null, + Guid? assignUserId = null) { var org = await _organizationRepository.GetByIdAsync(collection.OrganizationId); if(org == null) @@ -62,6 +63,18 @@ namespace Bit.Core.Services await _collectionRepository.CreateAsync(collection, groups); } + // Assign a user to the newly created collection. + if(assignUserId.HasValue) + { + var orgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, assignUserId.Value); + if(orgUser != null && orgUser.Status == Enums.OrganizationUserStatusType.Confirmed) + { + await _collectionRepository.UpdateUsersAsync(collection.Id, + new List { + new SelectionReadOnly { Id = orgUser.Id, ReadOnly = false } }); + } + } + await _eventService.LogCollectionEventAsync(collection, Enums.EventType.Collection_Created); } else diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 2a0feaec45..ace7180b4e 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -153,7 +153,6 @@ - @@ -233,5 +232,10 @@ + + + + + \ No newline at end of file diff --git a/src/Sql/dbo/Functions/UserCollectionDetails.sql b/src/Sql/dbo/Functions/UserCollectionDetails.sql new file mode 100644 index 0000000000..f8eb2d9cce --- /dev/null +++ b/src/Sql/dbo/Functions/UserCollectionDetails.sql @@ -0,0 +1,38 @@ +CREATE FUNCTION [dbo].[UserCollectionDetails](@UserId UNIQUEIDENTIFIER) +RETURNS TABLE +AS RETURN +SELECT + C.*, + CASE + WHEN + OU.[AccessAll] = 1 + OR G.[AccessAll] = 1 + OR CU.[ReadOnly] = 0 + OR CG.[ReadOnly] = 0 + THEN 0 + ELSE 1 + END [ReadOnly] +FROM + [dbo].[CollectionView] C +INNER JOIN + [dbo].[OrganizationUser] OU ON C.[OrganizationId] = OU.[OrganizationId] +INNER JOIN + [dbo].[Organization] O ON O.[Id] = C.[OrganizationId] +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.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId] +WHERE + OU.[UserId] = @UserId + AND OU.[Status] = 2 -- 2 = Confirmed + AND O.[Enabled] = 1 + AND ( + OU.[AccessAll] = 1 + OR CU.[CollectionId] IS NOT NULL + OR G.[AccessAll] = 1 + OR CG.[CollectionId] IS NOT NULL + ) diff --git a/src/Sql/dbo/Stored Procedures/Collection_ReadByIdUserId.sql b/src/Sql/dbo/Stored Procedures/Collection_ReadByIdUserId.sql new file mode 100644 index 0000000000..0987ad317c --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Collection_ReadByIdUserId.sql @@ -0,0 +1,15 @@ +CREATE PROCEDURE [dbo].[Collection_ReadByIdUserId] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + SELECT TOP 1 + * + FROM + [dbo].[UserCollectionDetails](@UserId) + WHERE + [Id] = @Id + ORDER BY + [ReadOnly] ASC +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Collection_ReadByUserId.sql b/src/Sql/dbo/Stored Procedures/Collection_ReadByUserId.sql index 16d89913bf..4a7e18d17a 100644 --- a/src/Sql/dbo/Stored Procedures/Collection_ReadByUserId.sql +++ b/src/Sql/dbo/Stored Procedures/Collection_ReadByUserId.sql @@ -5,38 +5,7 @@ BEGIN SET NOCOUNT ON SELECT - C.*, - CASE - WHEN - OU.[AccessAll] = 1 - OR G.[AccessAll] = 1 - OR CU.[ReadOnly] = 0 - OR CG.[ReadOnly] = 0 - THEN 0 - ELSE 1 - END [ReadOnly] + * FROM - [dbo].[CollectionView] C - INNER JOIN - [dbo].[OrganizationUser] OU ON C.[OrganizationId] = OU.[OrganizationId] - INNER JOIN - [dbo].[Organization] O ON O.[Id] = C.[OrganizationId] - 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.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId] - WHERE - OU.[UserId] = @UserId - AND OU.[Status] = 2 -- 2 = Confirmed - AND O.[Enabled] = 1 - AND ( - OU.[AccessAll] = 1 - OR CU.[CollectionId] IS NOT NULL - OR G.[AccessAll] = 1 - OR CG.[CollectionId] IS NOT NULL - ) + [dbo].[UserCollectionDetails](@UserId) END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Collection_ReadWithGroupsByIdUserId.sql b/src/Sql/dbo/Stored Procedures/Collection_ReadWithGroupsByIdUserId.sql new file mode 100644 index 0000000000..40ad021931 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Collection_ReadWithGroupsByIdUserId.sql @@ -0,0 +1,17 @@ +CREATE PROCEDURE [dbo].[Collection_ReadWithGroupsByIdUserId] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[Collection_ReadByIdUserId] @Id, @UserId + + SELECT + [GroupId] [Id], + [ReadOnly] + FROM + [dbo].[CollectionGroup] + WHERE + [CollectionId] = @Id +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Collection_UpdateUsers.sql b/src/Sql/dbo/Stored Procedures/Collection_UpdateUsers.sql new file mode 100644 index 0000000000..d8b2fce796 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Collection_UpdateUsers.sql @@ -0,0 +1,41 @@ +CREATE PROCEDURE [dbo].[Collection_UpdateUsers] + @Id UNIQUEIDENTIFIER, + @Users AS [dbo].[SelectionReadOnlyArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + DECLARE @OrganizationId UNIQUEIDENTIFIER = (SELECT [OrganizationId] FROM [dbo].[Collection] WHERE [Id] = @Id) + + ;WITH [AvailableUsersCTE] AS( + SELECT + Id + FROM + [dbo].[OrganizationUser] + WHERE + OrganizationId = @OrganizationId + ) + MERGE + [dbo].[CollectionUser] AS [Target] + USING + @Users AS [Source] + ON + [Target].[CollectionId] = @Id + AND [Target].[OrganizationUserId] = [Source].[Id] + WHEN NOT MATCHED BY TARGET + AND [Source].[Id] IN (SELECT [Id] FROM [AvailableUsersCTE]) THEN + INSERT VALUES + ( + @Id, + [Source].[Id], + [Source].[ReadOnly] + ) + WHEN MATCHED AND [Target].[ReadOnly] != [Source].[ReadOnly] THEN + UPDATE SET [Target].[ReadOnly] = [Source].[ReadOnly] + WHEN NOT MATCHED BY SOURCE + AND [Target].[CollectionId] = @Id THEN + DELETE + ; + + EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId +END \ No newline at end of file diff --git a/util/Setup/DbScripts/2018-10-17_00_ManagerRole.sql b/util/Setup/DbScripts/2018-10-17_00_ManagerRole.sql new file mode 100644 index 0000000000..660ad1921b --- /dev/null +++ b/util/Setup/DbScripts/2018-10-17_00_ManagerRole.sql @@ -0,0 +1,161 @@ +IF OBJECT_ID('[dbo].[UserCollectionDetails]') IS NOT NULL +BEGIN + DROP FUNCTION [dbo].[UserCollectionDetails] +END +GO + +CREATE FUNCTION [dbo].[UserCollectionDetails](@UserId UNIQUEIDENTIFIER) +RETURNS TABLE +AS RETURN +SELECT + C.*, + CASE + WHEN + OU.[AccessAll] = 1 + OR G.[AccessAll] = 1 + OR CU.[ReadOnly] = 0 + OR CG.[ReadOnly] = 0 + THEN 0 + ELSE 1 + END [ReadOnly] +FROM + [dbo].[CollectionView] C +INNER JOIN + [dbo].[OrganizationUser] OU ON C.[OrganizationId] = OU.[OrganizationId] +INNER JOIN + [dbo].[Organization] O ON O.[Id] = C.[OrganizationId] +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.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId] +WHERE + OU.[UserId] = @UserId + AND OU.[Status] = 2 -- 2 = Confirmed + AND O.[Enabled] = 1 + AND ( + OU.[AccessAll] = 1 + OR CU.[CollectionId] IS NOT NULL + OR G.[AccessAll] = 1 + OR CG.[CollectionId] IS NOT NULL + ) +GO + +IF OBJECT_ID('[dbo].[Collection_ReadByIdUserId]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Collection_ReadByIdUserId] +END +GO + +CREATE PROCEDURE [dbo].[Collection_ReadByIdUserId] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + SELECT TOP 1 + * + FROM + [dbo].[UserCollectionDetails](@UserId) + WHERE + [Id] = @Id + ORDER BY + [ReadOnly] ASC +END +GO + +IF OBJECT_ID('[dbo].[Collection_ReadByUserId]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Collection_ReadByUserId] +END +GO + +CREATE PROCEDURE [dbo].[Collection_ReadByUserId] + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[UserCollectionDetails](@UserId) +END +GO + +IF OBJECT_ID('[dbo].[Collection_ReadWithGroupsByIdUserId]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Collection_ReadWithGroupsByIdUserId] +END +GO + +CREATE PROCEDURE [dbo].[Collection_ReadWithGroupsByIdUserId] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[Collection_ReadByIdUserId] @Id, @UserId + + SELECT + [GroupId] [Id], + [ReadOnly] + FROM + [dbo].[CollectionGroup] + WHERE + [CollectionId] = @Id +END +GO + +IF OBJECT_ID('[dbo].[Collection_UpdateUsers]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Collection_UpdateUsers] +END +GO + +CREATE PROCEDURE [dbo].[Collection_UpdateUsers] + @Id UNIQUEIDENTIFIER, + @Users AS [dbo].[SelectionReadOnlyArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + DECLARE @OrganizationId UNIQUEIDENTIFIER = (SELECT [OrganizationId] FROM [dbo].[Collection] WHERE [Id] = @Id) + + ;WITH [AvailableUsersCTE] AS( + SELECT + Id + FROM + [dbo].[OrganizationUser] + WHERE + OrganizationId = @OrganizationId + ) + MERGE + [dbo].[CollectionUser] AS [Target] + USING + @Users AS [Source] + ON + [Target].[CollectionId] = @Id + AND [Target].[OrganizationUserId] = [Source].[Id] + WHEN NOT MATCHED BY TARGET + AND [Source].[Id] IN (SELECT [Id] FROM [AvailableUsersCTE]) THEN + INSERT VALUES + ( + @Id, + [Source].[Id], + [Source].[ReadOnly] + ) + WHEN MATCHED AND [Target].[ReadOnly] != [Source].[ReadOnly] THEN + UPDATE SET [Target].[ReadOnly] = [Source].[ReadOnly] + WHEN NOT MATCHED BY SOURCE + AND [Target].[CollectionId] = @Id THEN + DELETE + ; + + EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId +END +GO diff --git a/util/Setup/Setup.csproj b/util/Setup/Setup.csproj index bc76b4893a..1b780b6340 100644 --- a/util/Setup/Setup.csproj +++ b/util/Setup/Setup.csproj @@ -14,6 +14,7 @@ +