mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 08:02:49 -05:00
[AC-1174] Bulk Collection Management (#3229)
* [AC-1174] Update SelectionReadOnlyRequestModel to use Guid for Id property * [AC-1174] Introduce initial bulk-access collection endpoint * [AC-1174] Introduce BulkAddCollectionAccessCommand and validation logic/tests * [AC-1174] Add CreateOrUpdateAccessMany method to CollectionRepository * [AC-1174] Add event logs for bulk add collection access command * [AC-1174] Add User_BumpAccountRevisionDateByCollectionIds and database migration script * [AC-1174] Implement EF repository method * [AC-1174] Improve null checks * [AC-1174] Remove unnecessary BulkCollectionAccessRequestModel helpers * [AC-1174] Add unit tests for new controller endpoint * [AC-1174] Fix formatting * [AC-1174] Remove comment * [AC-1174] Remove redundant organizationId parameter * [AC-1174] Ensure user and group Ids are distinct * [AC-1174] Cleanup tests based on PR feedback * [AC-1174] Formatting * [AC-1174] Update CollectionGroup alias in the sproc * [AC-1174] Add some additional comments to SQL sproc * [AC-1174] Add comment explaining additional SaveChangesAsync call --------- Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
This commit is contained in:
@ -20,23 +20,26 @@ public class CollectionsController : Controller
|
||||
private readonly ICollectionService _collectionService;
|
||||
private readonly IDeleteCollectionCommand _deleteCollectionCommand;
|
||||
private readonly IUserService _userService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IBulkAddCollectionAccessCommand _bulkAddCollectionAccessCommand;
|
||||
|
||||
public CollectionsController(
|
||||
ICollectionRepository collectionRepository,
|
||||
ICollectionService collectionService,
|
||||
IDeleteCollectionCommand deleteCollectionCommand,
|
||||
IUserService userService,
|
||||
IAuthorizationService authorizationService,
|
||||
ICurrentContext currentContext,
|
||||
IAuthorizationService authorizationService)
|
||||
IBulkAddCollectionAccessCommand bulkAddCollectionAccessCommand)
|
||||
{
|
||||
_collectionRepository = collectionRepository;
|
||||
_collectionService = collectionService;
|
||||
_deleteCollectionCommand = deleteCollectionCommand;
|
||||
_userService = userService;
|
||||
_currentContext = currentContext;
|
||||
_authorizationService = authorizationService;
|
||||
_currentContext = currentContext;
|
||||
_bulkAddCollectionAccessCommand = bulkAddCollectionAccessCommand;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -190,6 +193,29 @@ public class CollectionsController : Controller
|
||||
await _collectionRepository.UpdateUsersAsync(collection.Id, model?.Select(g => g.ToSelectionReadOnly()));
|
||||
}
|
||||
|
||||
[HttpPost("bulk-access")]
|
||||
public async Task PostBulkCollectionAccess([FromBody] BulkCollectionAccessRequestModel model)
|
||||
{
|
||||
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.CollectionIds);
|
||||
|
||||
if (collections.Count != model.CollectionIds.Count())
|
||||
{
|
||||
throw new NotFoundException("One or more collections not found.");
|
||||
}
|
||||
|
||||
var result = await _authorizationService.AuthorizeAsync(User, collections, CollectionOperations.ModifyAccess);
|
||||
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _bulkAddCollectionAccessCommand.AddAccessAsync(
|
||||
collections,
|
||||
model.Users?.Select(u => u.ToSelectionReadOnly()).ToList(),
|
||||
model.Groups?.Select(g => g.ToSelectionReadOnly()).ToList());
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task Delete(Guid orgId, Guid id)
|
||||
|
@ -0,0 +1,8 @@
|
||||
namespace Bit.Api.Models.Request;
|
||||
|
||||
public class BulkCollectionAccessRequestModel
|
||||
{
|
||||
public IEnumerable<Guid> CollectionIds { get; set; }
|
||||
public IEnumerable<SelectionReadOnlyRequestModel> Groups { get; set; }
|
||||
public IEnumerable<SelectionReadOnlyRequestModel> Users { get; set; }
|
||||
}
|
@ -6,7 +6,7 @@ namespace Bit.Api.Models.Request;
|
||||
public class SelectionReadOnlyRequestModel
|
||||
{
|
||||
[Required]
|
||||
public string Id { get; set; }
|
||||
public Guid Id { get; set; }
|
||||
public bool ReadOnly { get; set; }
|
||||
public bool HidePasswords { get; set; }
|
||||
public bool Manage { get; set; }
|
||||
@ -15,7 +15,7 @@ public class SelectionReadOnlyRequestModel
|
||||
{
|
||||
return new CollectionAccessSelection
|
||||
{
|
||||
Id = new Guid(Id),
|
||||
Id = Id,
|
||||
ReadOnly = ReadOnly,
|
||||
HidePasswords = HidePasswords,
|
||||
Manage = Manage,
|
||||
|
@ -0,0 +1,95 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures.OrganizationCollections;
|
||||
|
||||
public class BulkAddCollectionAccessCommand : IBulkAddCollectionAccessCommand
|
||||
{
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly IEventService _eventService;
|
||||
|
||||
public BulkAddCollectionAccessCommand(
|
||||
ICollectionRepository collectionRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IGroupRepository groupRepository,
|
||||
IEventService eventService)
|
||||
{
|
||||
_collectionRepository = collectionRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_groupRepository = groupRepository;
|
||||
_eventService = eventService;
|
||||
}
|
||||
|
||||
public async Task AddAccessAsync(ICollection<Collection> collections,
|
||||
ICollection<CollectionAccessSelection> users,
|
||||
ICollection<CollectionAccessSelection> groups)
|
||||
{
|
||||
await ValidateRequestAsync(collections, users, groups);
|
||||
|
||||
await _collectionRepository.CreateOrUpdateAccessForManyAsync(
|
||||
collections.First().OrganizationId,
|
||||
collections.Select(c => c.Id),
|
||||
users,
|
||||
groups
|
||||
);
|
||||
|
||||
await _eventService.LogCollectionEventsAsync(collections.Select(c =>
|
||||
(c, EventType.Collection_Updated, (DateTime?)DateTime.UtcNow)));
|
||||
}
|
||||
|
||||
private async Task ValidateRequestAsync(ICollection<Collection> collections, ICollection<CollectionAccessSelection> usersAccess, ICollection<CollectionAccessSelection> groupsAccess)
|
||||
{
|
||||
if (collections == null || collections.Count == 0)
|
||||
{
|
||||
throw new BadRequestException("No collections were provided.");
|
||||
}
|
||||
|
||||
var orgId = collections.First().OrganizationId;
|
||||
|
||||
if (collections.Any(c => c.OrganizationId != orgId))
|
||||
{
|
||||
throw new BadRequestException("All collections must belong to the same organization.");
|
||||
}
|
||||
|
||||
var collectionUserIds = usersAccess?.Select(u => u.Id).Distinct().ToList();
|
||||
|
||||
if (collectionUserIds is { Count: > 0 })
|
||||
{
|
||||
var users = await _organizationUserRepository.GetManyAsync(collectionUserIds);
|
||||
|
||||
if (users.Count != collectionUserIds.Count)
|
||||
{
|
||||
throw new BadRequestException("One or more users do not exist.");
|
||||
}
|
||||
|
||||
if (users.Any(u => u.OrganizationId != orgId))
|
||||
{
|
||||
throw new BadRequestException("One or more users do not belong to the same organization as the collection being assigned.");
|
||||
}
|
||||
}
|
||||
|
||||
var collectionGroupIds = groupsAccess?.Select(g => g.Id).Distinct().ToList();
|
||||
|
||||
if (collectionGroupIds is { Count: > 0 })
|
||||
{
|
||||
var groups = await _groupRepository.GetManyByManyIds(collectionGroupIds);
|
||||
|
||||
if (groups.Count != collectionGroupIds.Count)
|
||||
{
|
||||
throw new BadRequestException("One or more groups do not exist.");
|
||||
}
|
||||
|
||||
if (groups.Any(g => g.OrganizationId != orgId))
|
||||
{
|
||||
throw new BadRequestException("One or more groups do not belong to the same organization as the collection being assigned.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
|
||||
|
||||
public interface IBulkAddCollectionAccessCommand
|
||||
{
|
||||
Task AddAccessAsync(ICollection<Collection> collections,
|
||||
ICollection<CollectionAccessSelection> users, ICollection<CollectionAccessSelection> groups);
|
||||
}
|
@ -98,6 +98,7 @@ public static class OrganizationServiceCollectionExtensions
|
||||
public static void AddOrganizationCollectionCommands(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IDeleteCollectionCommand, DeleteCollectionCommand>();
|
||||
services.AddScoped<IBulkAddCollectionAccessCommand, BulkAddCollectionAccessCommand>();
|
||||
}
|
||||
|
||||
private static void AddOrganizationGroupCommands(this IServiceCollection services)
|
||||
|
@ -20,4 +20,6 @@ public interface ICollectionRepository : IRepository<Collection, Guid>
|
||||
Task UpdateUsersAsync(Guid id, IEnumerable<CollectionAccessSelection> users);
|
||||
Task<ICollection<CollectionAccessSelection>> GetManyUsersByIdAsync(Guid id);
|
||||
Task DeleteManyAsync(IEnumerable<Guid> collectionIds);
|
||||
Task CreateOrUpdateAccessForManyAsync(Guid organizationId, IEnumerable<Guid> collectionIds,
|
||||
IEnumerable<CollectionAccessSelection> users, IEnumerable<CollectionAccessSelection> groups);
|
||||
}
|
||||
|
@ -252,6 +252,21 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateOrUpdateAccessForManyAsync(Guid organizationId, IEnumerable<Guid> collectionIds,
|
||||
IEnumerable<CollectionAccessSelection> users, IEnumerable<CollectionAccessSelection> groups)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var usersArray = users != null ? users.ToArrayTVP() : Enumerable.Empty<CollectionAccessSelection>().ToArrayTVP();
|
||||
var groupsArray = groups != null ? groups.ToArrayTVP() : Enumerable.Empty<CollectionAccessSelection>().ToArrayTVP();
|
||||
|
||||
var results = await connection.ExecuteAsync(
|
||||
$"[{Schema}].[Collection_CreateOrUpdateAccessForMany]",
|
||||
new { OrganizationId = organizationId, CollectionIds = collectionIds.ToGuidIdArrayTVP(), Users = usersArray, Groups = groupsArray },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateUserAsync(Guid collectionId, Guid organizationUserId)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
|
@ -473,6 +473,97 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateOrUpdateAccessForManyAsync(Guid organizationId, IEnumerable<Guid> collectionIds,
|
||||
IEnumerable<CollectionAccessSelection> users, IEnumerable<CollectionAccessSelection> groups)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var collectionIdsList = collectionIds.ToList();
|
||||
|
||||
if (users != null)
|
||||
{
|
||||
var existingCollectionUsers = await dbContext.CollectionUsers
|
||||
.Where(cu => collectionIdsList.Contains(cu.CollectionId))
|
||||
.ToDictionaryAsync(x => (x.CollectionId, x.OrganizationUserId));
|
||||
|
||||
var requestedUsers = users.ToList();
|
||||
|
||||
foreach (var collectionId in collectionIdsList)
|
||||
{
|
||||
foreach (var requestedUser in requestedUsers)
|
||||
{
|
||||
if (!existingCollectionUsers.TryGetValue(
|
||||
(collectionId, requestedUser.Id),
|
||||
out var existingCollectionUser)
|
||||
)
|
||||
{
|
||||
// This is a brand new entry
|
||||
dbContext.CollectionUsers.Add(new CollectionUser
|
||||
{
|
||||
CollectionId = collectionId,
|
||||
OrganizationUserId = requestedUser.Id,
|
||||
HidePasswords = requestedUser.HidePasswords,
|
||||
ReadOnly = requestedUser.ReadOnly,
|
||||
Manage = requestedUser.Manage
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// It already exists, update it
|
||||
existingCollectionUser.HidePasswords = requestedUser.HidePasswords;
|
||||
existingCollectionUser.ReadOnly = requestedUser.ReadOnly;
|
||||
existingCollectionUser.Manage = requestedUser.Manage;
|
||||
dbContext.CollectionUsers.Update(existingCollectionUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (groups != null)
|
||||
{
|
||||
var existingCollectionGroups = await dbContext.CollectionGroups
|
||||
.Where(cu => collectionIdsList.Contains(cu.CollectionId))
|
||||
.ToDictionaryAsync(x => (x.CollectionId, x.GroupId));
|
||||
|
||||
var requestedGroups = groups.ToList();
|
||||
|
||||
foreach (var collectionId in collectionIdsList)
|
||||
{
|
||||
foreach (var requestedGroup in requestedGroups)
|
||||
{
|
||||
if (!existingCollectionGroups.TryGetValue(
|
||||
(collectionId, requestedGroup.Id),
|
||||
out var existingCollectionGroup)
|
||||
)
|
||||
{
|
||||
// This is a brand new entry
|
||||
dbContext.CollectionGroups.Add(new CollectionGroup()
|
||||
{
|
||||
CollectionId = collectionId,
|
||||
GroupId = requestedGroup.Id,
|
||||
HidePasswords = requestedGroup.HidePasswords,
|
||||
ReadOnly = requestedGroup.ReadOnly,
|
||||
Manage = requestedGroup.Manage
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// It already exists, update it
|
||||
existingCollectionGroup.HidePasswords = requestedGroup.HidePasswords;
|
||||
existingCollectionGroup.ReadOnly = requestedGroup.ReadOnly;
|
||||
existingCollectionGroup.Manage = requestedGroup.Manage;
|
||||
dbContext.CollectionGroups.Update(existingCollectionGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Need to save the new collection users/groups before running the bump revision code
|
||||
await dbContext.SaveChangesAsync();
|
||||
await dbContext.UserBumpAccountRevisionDateByCollectionIdsAsync(collectionIdsList, organizationId);
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReplaceCollectionGroupsAsync(DatabaseContext dbContext, Core.Entities.Collection collection, IEnumerable<CollectionAccessSelection> groups)
|
||||
{
|
||||
var groupsInOrg = dbContext.Groups.Where(g => g.OrganizationId == collection.OrganizationId);
|
||||
|
@ -74,6 +74,39 @@ public static class DatabaseContextExtensions
|
||||
UpdateUserRevisionDate(users);
|
||||
}
|
||||
|
||||
public static async Task UserBumpAccountRevisionDateByCollectionIdsAsync(this DatabaseContext context, IEnumerable<Guid> collectionIds, Guid organizationId)
|
||||
{
|
||||
var query = from u in context.Users
|
||||
from c in context.Collections
|
||||
join ou in context.OrganizationUsers
|
||||
on u.Id equals ou.UserId
|
||||
join cu in context.CollectionUsers
|
||||
on new { ou.AccessAll, OrganizationUserId = ou.Id, CollectionId = c.Id } equals
|
||||
new { AccessAll = false, cu.OrganizationUserId, cu.CollectionId } into cu_g
|
||||
from cu in cu_g.DefaultIfEmpty()
|
||||
join gu in context.GroupUsers
|
||||
on new { CollectionId = (Guid?)cu.CollectionId, ou.AccessAll, OrganizationUserId = ou.Id } equals
|
||||
new { CollectionId = (Guid?)null, AccessAll = false, gu.OrganizationUserId } into gu_g
|
||||
from gu in gu_g.DefaultIfEmpty()
|
||||
join g in context.Groups
|
||||
on gu.GroupId equals g.Id into g_g
|
||||
from g in g_g.DefaultIfEmpty()
|
||||
join cg in context.CollectionGroups
|
||||
on new { g.AccessAll, gu.GroupId, CollectionId = c.Id } equals
|
||||
new { AccessAll = false, cg.GroupId, cg.CollectionId } into cg_g
|
||||
from cg in cg_g.DefaultIfEmpty()
|
||||
where ou.OrganizationId == organizationId && collectionIds.Contains(c.Id) &&
|
||||
ou.Status == OrganizationUserStatusType.Confirmed &&
|
||||
(cu.CollectionId != null ||
|
||||
cg.CollectionId != null ||
|
||||
ou.AccessAll == true ||
|
||||
g.AccessAll == true)
|
||||
select u;
|
||||
|
||||
var users = await query.ToListAsync();
|
||||
UpdateUserRevisionDate(users);
|
||||
}
|
||||
|
||||
public static async Task UserBumpAccountRevisionDateByOrganizationUserIdAsync(this DatabaseContext context, Guid organizationUserId)
|
||||
{
|
||||
var query = from u in context.Users
|
||||
|
@ -0,0 +1,97 @@
|
||||
CREATE PROCEDURE [dbo].[Collection_CreateOrUpdateAccessForMany]
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@CollectionIds AS [dbo].[GuidIdArray] READONLY,
|
||||
@Groups AS [dbo].[SelectionReadOnlyArray] READONLY,
|
||||
@Users AS [dbo].[SelectionReadOnlyArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
-- Groups
|
||||
;WITH [NewCollectionGroups] AS (
|
||||
SELECT
|
||||
cId.[Id] AS [CollectionId],
|
||||
cg.[Id] AS [GroupId],
|
||||
cg.[ReadOnly],
|
||||
cg.[HidePasswords],
|
||||
cg.[Manage]
|
||||
FROM
|
||||
@Groups AS cg
|
||||
CROSS JOIN -- Create a CollectionGroup record for every CollectionId
|
||||
@CollectionIds cId
|
||||
INNER JOIN
|
||||
[dbo].[Group] g ON cg.[Id] = g.[Id]
|
||||
WHERE
|
||||
g.[OrganizationId] = @OrganizationId
|
||||
)
|
||||
MERGE
|
||||
[dbo].[CollectionGroup] as [Target]
|
||||
USING
|
||||
[NewCollectionGroups] AS [Source]
|
||||
ON
|
||||
[Target].[CollectionId] = [Source].[CollectionId]
|
||||
AND [Target].[GroupId] = [Source].[GroupId]
|
||||
-- Update the target if any values are different from the source
|
||||
WHEN MATCHED AND EXISTS(
|
||||
SELECT [Source].[ReadOnly], [Source].[HidePasswords], [Source].[Manage]
|
||||
EXCEPT
|
||||
SELECT [Target].[ReadOnly], [Target].[HidePasswords], [Target].[Manage]
|
||||
) THEN UPDATE SET
|
||||
[Target].[ReadOnly] = [Source].[ReadOnly],
|
||||
[Target].[HidePasswords] = [Source].[HidePasswords],
|
||||
[Target].[Manage] = [Source].[Manage]
|
||||
WHEN NOT MATCHED BY TARGET
|
||||
THEN INSERT VALUES
|
||||
(
|
||||
[Source].[CollectionId],
|
||||
[Source].[GroupId],
|
||||
[Source].[ReadOnly],
|
||||
[Source].[HidePasswords],
|
||||
[Source].[Manage]
|
||||
);
|
||||
|
||||
-- Users
|
||||
;WITH [NewCollectionUsers] AS (
|
||||
SELECT
|
||||
cId.[Id] AS [CollectionId],
|
||||
cu.[Id] AS [OrganizationUserId],
|
||||
cu.[ReadOnly],
|
||||
cu.[HidePasswords],
|
||||
cu.[Manage]
|
||||
FROM
|
||||
@Users AS cu
|
||||
CROSS JOIN -- Create a CollectionUser record for every CollectionId
|
||||
@CollectionIds cId
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] u ON cu.[Id] = u.[Id]
|
||||
WHERE
|
||||
u.[OrganizationId] = @OrganizationId
|
||||
)
|
||||
MERGE
|
||||
[dbo].[CollectionUser] as [Target]
|
||||
USING
|
||||
[NewCollectionUsers] AS [Source]
|
||||
ON
|
||||
[Target].[CollectionId] = [Source].[CollectionId]
|
||||
AND [Target].[OrganizationUserId] = [Source].[OrganizationUserId]
|
||||
-- Update the target if any values are different from the source
|
||||
WHEN MATCHED AND EXISTS(
|
||||
SELECT [Source].[ReadOnly], [Source].[HidePasswords], [Source].[Manage]
|
||||
EXCEPT
|
||||
SELECT [Target].[ReadOnly], [Target].[HidePasswords], [Target].[Manage]
|
||||
) THEN UPDATE SET
|
||||
[Target].[ReadOnly] = [Source].[ReadOnly],
|
||||
[Target].[HidePasswords] = [Source].[HidePasswords],
|
||||
[Target].[Manage] = [Source].[Manage]
|
||||
WHEN NOT MATCHED BY TARGET
|
||||
THEN INSERT VALUES
|
||||
(
|
||||
[Source].[CollectionId],
|
||||
[Source].[OrganizationUserId],
|
||||
[Source].[ReadOnly],
|
||||
[Source].[HidePasswords],
|
||||
[Source].[Manage]
|
||||
);
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionIds] @CollectionIds, @OrganizationId
|
||||
END
|
@ -0,0 +1,35 @@
|
||||
CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDateByCollectionIds]
|
||||
@CollectionIds AS [dbo].[GuidIdArray] READONLY,
|
||||
@OrganizationId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
U
|
||||
SET
|
||||
U.[AccountRevisionDate] = GETUTCDATE()
|
||||
FROM
|
||||
[dbo].[User] U
|
||||
INNER JOIN
|
||||
[dbo].[Collection] C ON C.[Id] IN (SELECT [Id] FROM @CollectionIds)
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] OU ON OU.[UserId] = U.[Id]
|
||||
LEFT JOIN
|
||||
[dbo].[CollectionUser] CU ON OU.[AccessAll] = 0 AND CU.[OrganizationUserId] = OU.[Id] AND CU.[CollectionId] = C.[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] AND CG.[CollectionId] = C.[Id]
|
||||
WHERE
|
||||
OU.[OrganizationId] = @OrganizationId
|
||||
AND OU.[Status] = 2 -- 2 = Confirmed
|
||||
AND (
|
||||
CU.[CollectionId] IS NOT NULL
|
||||
OR CG.[CollectionId] IS NOT NULL
|
||||
OR OU.[AccessAll] = 1
|
||||
OR G.[AccessAll] = 1
|
||||
)
|
||||
END
|
@ -221,4 +221,120 @@ public class CollectionsControllerTests
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.DeleteManyAsync((IEnumerable<Collection>)default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostBulkCollectionAccess_Success(User actingUser, ICollection<Collection> collections, SutProvider<CollectionsController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var groupId = Guid.NewGuid();
|
||||
var model = new BulkCollectionAccessRequestModel
|
||||
{
|
||||
CollectionIds = collections.Select(c => c.Id),
|
||||
Users = new[] { new SelectionReadOnlyRequestModel { Id = userId, Manage = true } },
|
||||
Groups = new[] { new SelectionReadOnlyRequestModel { Id = groupId, ReadOnly = true } },
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICollectionRepository>()
|
||||
.GetManyByManyIdsAsync(model.CollectionIds)
|
||||
.Returns(collections);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.UserId.Returns(actingUser.Id);
|
||||
|
||||
sutProvider.GetDependency<IAuthorizationService>().AuthorizeAsync(
|
||||
Arg.Any<ClaimsPrincipal>(), ExpectedCollectionAccess(),
|
||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(
|
||||
r => r.Contains(CollectionOperations.ModifyAccess)
|
||||
))
|
||||
.Returns(AuthorizationResult.Success());
|
||||
|
||||
IEnumerable<Collection> ExpectedCollectionAccess() => Arg.Is<IEnumerable<Collection>>(cols => cols.SequenceEqual(collections));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PostBulkCollectionAccess(model);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IAuthorizationService>().Received().AuthorizeAsync(
|
||||
Arg.Any<ClaimsPrincipal>(),
|
||||
ExpectedCollectionAccess(),
|
||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(
|
||||
r => r.Contains(CollectionOperations.ModifyAccess))
|
||||
);
|
||||
await sutProvider.GetDependency<IBulkAddCollectionAccessCommand>().Received()
|
||||
.AddAccessAsync(
|
||||
Arg.Is<ICollection<Collection>>(g => g.SequenceEqual(collections)),
|
||||
Arg.Is<ICollection<CollectionAccessSelection>>(u => u.All(c => c.Id == userId && c.Manage)),
|
||||
Arg.Is<ICollection<CollectionAccessSelection>>(g => g.All(c => c.Id == groupId && c.ReadOnly)));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostBulkCollectionAccess_CollectionsNotFound_Throws(User actingUser, ICollection<Collection> collections, SutProvider<CollectionsController> sutProvider)
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var groupId = Guid.NewGuid();
|
||||
var model = new BulkCollectionAccessRequestModel
|
||||
{
|
||||
CollectionIds = collections.Select(c => c.Id),
|
||||
Users = new[] { new SelectionReadOnlyRequestModel { Id = userId, Manage = true } },
|
||||
Groups = new[] { new SelectionReadOnlyRequestModel { Id = groupId, ReadOnly = true } },
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.UserId.Returns(actingUser.Id);
|
||||
|
||||
sutProvider.GetDependency<ICollectionRepository>()
|
||||
.GetManyByManyIdsAsync(model.CollectionIds)
|
||||
.Returns(collections.Skip(1).ToList());
|
||||
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PostBulkCollectionAccess(model));
|
||||
|
||||
Assert.Equal("One or more collections not found.", exception.Message);
|
||||
await sutProvider.GetDependency<IAuthorizationService>().DidNotReceiveWithAnyArgs().AuthorizeAsync(
|
||||
Arg.Any<ClaimsPrincipal>(),
|
||||
Arg.Any<IEnumerable<Collection>>(),
|
||||
Arg.Any<IEnumerable<IAuthorizationRequirement>>()
|
||||
);
|
||||
await sutProvider.GetDependency<IBulkAddCollectionAccessCommand>().DidNotReceiveWithAnyArgs()
|
||||
.AddAccessAsync(default, default, default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostBulkCollectionAccess_AccessDenied_Throws(User actingUser, ICollection<Collection> collections, SutProvider<CollectionsController> sutProvider)
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var groupId = Guid.NewGuid();
|
||||
var model = new BulkCollectionAccessRequestModel
|
||||
{
|
||||
CollectionIds = collections.Select(c => c.Id),
|
||||
Users = new[] { new SelectionReadOnlyRequestModel { Id = userId, Manage = true } },
|
||||
Groups = new[] { new SelectionReadOnlyRequestModel { Id = groupId, ReadOnly = true } },
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.UserId.Returns(actingUser.Id);
|
||||
|
||||
sutProvider.GetDependency<ICollectionRepository>()
|
||||
.GetManyByManyIdsAsync(model.CollectionIds)
|
||||
.Returns(collections);
|
||||
|
||||
sutProvider.GetDependency<IAuthorizationService>().AuthorizeAsync(
|
||||
Arg.Any<ClaimsPrincipal>(), ExpectedCollectionAccess(),
|
||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(
|
||||
r => r.Contains(CollectionOperations.ModifyAccess)
|
||||
))
|
||||
.Returns(AuthorizationResult.Failed());
|
||||
|
||||
IEnumerable<Collection> ExpectedCollectionAccess() => Arg.Is<IEnumerable<Collection>>(cols => cols.SequenceEqual(collections));
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PostBulkCollectionAccess(model));
|
||||
await sutProvider.GetDependency<IAuthorizationService>().Received().AuthorizeAsync(
|
||||
Arg.Any<ClaimsPrincipal>(),
|
||||
ExpectedCollectionAccess(),
|
||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(
|
||||
r => r.Contains(CollectionOperations.ModifyAccess))
|
||||
);
|
||||
await sutProvider.GetDependency<IBulkAddCollectionAccessCommand>().DidNotReceiveWithAnyArgs()
|
||||
.AddAccessAsync(default, default, default);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,271 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationCollections;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.Vault.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.OrganizationFeatures.OrganizationCollections;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class BulkAddCollectionAccessCommandTests
|
||||
{
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task AddAccessAsync_Success(SutProvider<BulkAddCollectionAccessCommand> sutProvider,
|
||||
Organization org,
|
||||
ICollection<Collection> collections,
|
||||
ICollection<OrganizationUser> organizationUsers,
|
||||
ICollection<Group> groups,
|
||||
IEnumerable<CollectionUser> collectionUsers,
|
||||
IEnumerable<CollectionGroup> collectionGroups)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))
|
||||
)
|
||||
.Returns(organizationUsers);
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyByManyIds(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionGroups.Select(u => u.GroupId)))
|
||||
)
|
||||
.Returns(groups);
|
||||
|
||||
var userAccessSelections = ToAccessSelection(collectionUsers);
|
||||
var groupAccessSelections = ToAccessSelection(collectionGroups);
|
||||
await sutProvider.Sut.AddAccessAsync(collections,
|
||||
userAccessSelections,
|
||||
groupAccessSelections
|
||||
);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received().GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(userAccessSelections.Select(u => u.Id)))
|
||||
);
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received().GetManyByManyIds(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(groupAccessSelections.Select(g => g.Id)))
|
||||
);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received().CreateOrUpdateAccessForManyAsync(
|
||||
org.Id,
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collections.Select(c => c.Id))),
|
||||
userAccessSelections,
|
||||
groupAccessSelections);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>().Received().LogCollectionEventsAsync(
|
||||
Arg.Is<IEnumerable<(Collection, EventType, DateTime?)>>(
|
||||
events => events.All(e =>
|
||||
collections.Contains(e.Item1) &&
|
||||
e.Item2 == EventType.Collection_Updated &&
|
||||
e.Item3.HasValue
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task ValidateRequestAsync_NoCollectionsProvided_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider)
|
||||
{
|
||||
var exception =
|
||||
await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.AddAccessAsync(null, null, null));
|
||||
|
||||
Assert.Contains("No collections were provided.", exception.Message);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs().GetManyByManyIdsAsync(default);
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetManyAsync(default);
|
||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyByManyIds(default);
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task ValidateRequestAsync_NoCollection_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider,
|
||||
IEnumerable<CollectionUser> collectionUsers,
|
||||
IEnumerable<CollectionGroup> collectionGroups)
|
||||
{
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddAccessAsync(Enumerable.Empty<Collection>().ToList(),
|
||||
ToAccessSelection(collectionUsers),
|
||||
ToAccessSelection(collectionGroups)
|
||||
));
|
||||
|
||||
Assert.Contains("No collections were provided.", exception.Message);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetManyAsync(default);
|
||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyByManyIds(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task ValidateRequestAsync_DifferentOrgs_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider,
|
||||
ICollection<Collection> collections,
|
||||
IEnumerable<CollectionUser> collectionUsers,
|
||||
IEnumerable<CollectionGroup> collectionGroups)
|
||||
{
|
||||
collections.First().OrganizationId = Guid.NewGuid();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddAccessAsync(collections,
|
||||
ToAccessSelection(collectionUsers),
|
||||
ToAccessSelection(collectionGroups)
|
||||
));
|
||||
|
||||
Assert.Contains("All collections must belong to the same organization.", exception.Message);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetManyAsync(default);
|
||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyByManyIds(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task ValidateRequestAsync_MissingUser_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider,
|
||||
IList<Collection> collections,
|
||||
IList<OrganizationUser> organizationUsers,
|
||||
IEnumerable<CollectionUser> collectionUsers,
|
||||
IEnumerable<CollectionGroup> collectionGroups)
|
||||
{
|
||||
organizationUsers.RemoveAt(0);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))
|
||||
)
|
||||
.Returns(organizationUsers);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddAccessAsync(collections,
|
||||
ToAccessSelection(collectionUsers),
|
||||
ToAccessSelection(collectionGroups)
|
||||
));
|
||||
|
||||
Assert.Contains("One or more users do not exist.", exception.Message);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received().GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))
|
||||
);
|
||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyByManyIds(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task ValidateRequestAsync_UserWrongOrg_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider,
|
||||
IList<Collection> collections,
|
||||
IList<OrganizationUser> organizationUsers,
|
||||
IEnumerable<CollectionUser> collectionUsers,
|
||||
IEnumerable<CollectionGroup> collectionGroups)
|
||||
{
|
||||
organizationUsers.First().OrganizationId = Guid.NewGuid();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))
|
||||
)
|
||||
.Returns(organizationUsers);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddAccessAsync(collections,
|
||||
ToAccessSelection(collectionUsers),
|
||||
ToAccessSelection(collectionGroups)
|
||||
));
|
||||
|
||||
Assert.Contains("One or more users do not belong to the same organization as the collection being assigned.", exception.Message);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received().GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))
|
||||
);
|
||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyByManyIds(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task ValidateRequestAsync_MissingGroup_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider,
|
||||
IList<Collection> collections,
|
||||
IList<OrganizationUser> organizationUsers,
|
||||
IList<Group> groups,
|
||||
IEnumerable<CollectionUser> collectionUsers,
|
||||
IEnumerable<CollectionGroup> collectionGroups)
|
||||
{
|
||||
groups.RemoveAt(0);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))
|
||||
)
|
||||
.Returns(organizationUsers);
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyByManyIds(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionGroups.Select(u => u.GroupId)))
|
||||
)
|
||||
.Returns(groups);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddAccessAsync(collections,
|
||||
ToAccessSelection(collectionUsers),
|
||||
ToAccessSelection(collectionGroups)
|
||||
));
|
||||
|
||||
Assert.Contains("One or more groups do not exist.", exception.Message);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received().GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))
|
||||
);
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received().GetManyByManyIds(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionGroups.Select(u => u.GroupId)))
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task ValidateRequestAsync_GroupWrongOrg_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider,
|
||||
IList<Collection> collections,
|
||||
IList<OrganizationUser> organizationUsers,
|
||||
IList<Group> groups,
|
||||
IEnumerable<CollectionUser> collectionUsers,
|
||||
IEnumerable<CollectionGroup> collectionGroups)
|
||||
{
|
||||
groups.First().OrganizationId = Guid.NewGuid();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))
|
||||
)
|
||||
.Returns(organizationUsers);
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyByManyIds(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionGroups.Select(u => u.GroupId)))
|
||||
)
|
||||
.Returns(groups);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddAccessAsync(collections,
|
||||
ToAccessSelection(collectionUsers),
|
||||
ToAccessSelection(collectionGroups)
|
||||
));
|
||||
|
||||
Assert.Contains("One or more groups do not belong to the same organization as the collection being assigned.", exception.Message);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received().GetManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))
|
||||
);
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received().GetManyByManyIds(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionGroups.Select(u => u.GroupId)))
|
||||
);
|
||||
}
|
||||
|
||||
private static ICollection<CollectionAccessSelection> ToAccessSelection(IEnumerable<CollectionUser> collectionUsers)
|
||||
{
|
||||
return collectionUsers.Select(cu => new CollectionAccessSelection
|
||||
{
|
||||
Id = cu.OrganizationUserId,
|
||||
Manage = cu.Manage,
|
||||
HidePasswords = cu.HidePasswords,
|
||||
ReadOnly = cu.ReadOnly
|
||||
}).ToList();
|
||||
}
|
||||
private static ICollection<CollectionAccessSelection> ToAccessSelection(IEnumerable<CollectionGroup> collectionGroups)
|
||||
{
|
||||
return collectionGroups.Select(cg => new CollectionAccessSelection
|
||||
{
|
||||
Id = cg.GroupId,
|
||||
Manage = cg.Manage,
|
||||
HidePasswords = cg.HidePasswords,
|
||||
ReadOnly = cg.ReadOnly
|
||||
}).ToList();
|
||||
}
|
||||
}
|
@ -17,6 +17,9 @@ public class CollectionCustomization : ICustomization
|
||||
{
|
||||
var orgId = Guid.NewGuid();
|
||||
|
||||
fixture.Customize<Organization>(composer => composer
|
||||
.With(o => o.Id, orgId));
|
||||
|
||||
fixture.Customize<CurrentContextOrganization>(composer => composer
|
||||
.With(o => o.Id, orgId));
|
||||
|
||||
|
@ -0,0 +1,135 @@
|
||||
CREATE OR ALTER PROCEDURE [dbo].[User_BumpAccountRevisionDateByCollectionIds]
|
||||
@CollectionIds AS [dbo].[GuidIdArray] READONLY,
|
||||
@OrganizationId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
U
|
||||
SET
|
||||
U.[AccountRevisionDate] = GETUTCDATE()
|
||||
FROM
|
||||
[dbo].[User] U
|
||||
INNER JOIN
|
||||
[dbo].[Collection] C ON C.[Id] IN (SELECT [Id] FROM @CollectionIds)
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] OU ON OU.[UserId] = U.[Id]
|
||||
LEFT JOIN
|
||||
[dbo].[CollectionUser] CU ON OU.[AccessAll] = 0 AND CU.[OrganizationUserId] = OU.[Id] AND CU.[CollectionId] = C.[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] AND CG.[CollectionId] = C.[Id]
|
||||
WHERE
|
||||
OU.[OrganizationId] = @OrganizationId
|
||||
AND OU.[Status] = 2 -- 2 = Confirmed
|
||||
AND (
|
||||
CU.[CollectionId] IS NOT NULL
|
||||
OR CG.[CollectionId] IS NOT NULL
|
||||
OR OU.[AccessAll] = 1
|
||||
OR G.[AccessAll] = 1
|
||||
)
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Collection_CreateOrUpdateAccessForMany]
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@CollectionIds AS [dbo].[GuidIdArray] READONLY,
|
||||
@Groups AS [dbo].[SelectionReadOnlyArray] READONLY,
|
||||
@Users AS [dbo].[SelectionReadOnlyArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
-- Groups
|
||||
;WITH [NewCollectionGroups] AS (
|
||||
SELECT
|
||||
cId.[Id] AS [CollectionId],
|
||||
cg.[Id] AS [GroupId],
|
||||
cg.[ReadOnly],
|
||||
cg.[HidePasswords],
|
||||
cg.[Manage]
|
||||
FROM
|
||||
@Groups AS cg
|
||||
CROSS JOIN -- Create a CollectionGroup record for every CollectionId
|
||||
@CollectionIds cId
|
||||
INNER JOIN
|
||||
[dbo].[Group] g ON cg.[Id] = g.[Id]
|
||||
WHERE
|
||||
g.[OrganizationId] = @OrganizationId
|
||||
)
|
||||
MERGE
|
||||
[dbo].[CollectionGroup] as [Target]
|
||||
USING
|
||||
[NewCollectionGroups] AS [Source]
|
||||
ON
|
||||
[Target].[CollectionId] = [Source].[CollectionId]
|
||||
AND [Target].[GroupId] = [Source].[GroupId]
|
||||
-- Update the target if any values are different from the source
|
||||
WHEN MATCHED AND EXISTS(
|
||||
SELECT [Source].[ReadOnly], [Source].[HidePasswords], [Source].[Manage]
|
||||
EXCEPT
|
||||
SELECT [Target].[ReadOnly], [Target].[HidePasswords], [Target].[Manage]
|
||||
) THEN UPDATE SET
|
||||
[Target].[ReadOnly] = [Source].[ReadOnly],
|
||||
[Target].[HidePasswords] = [Source].[HidePasswords],
|
||||
[Target].[Manage] = [Source].[Manage]
|
||||
WHEN NOT MATCHED BY TARGET
|
||||
THEN INSERT VALUES
|
||||
(
|
||||
[Source].[CollectionId],
|
||||
[Source].[GroupId],
|
||||
[Source].[ReadOnly],
|
||||
[Source].[HidePasswords],
|
||||
[Source].[Manage]
|
||||
);
|
||||
|
||||
-- Users
|
||||
;WITH [NewCollectionUsers] AS (
|
||||
SELECT
|
||||
cId.[Id] AS [CollectionId],
|
||||
cu.[Id] AS [OrganizationUserId],
|
||||
cu.[ReadOnly],
|
||||
cu.[HidePasswords],
|
||||
cu.[Manage]
|
||||
FROM
|
||||
@Users AS cu
|
||||
CROSS JOIN -- Create a CollectionUser record for every CollectionId
|
||||
@CollectionIds cId
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] u ON cu.[Id] = u.[Id]
|
||||
WHERE
|
||||
u.[OrganizationId] = @OrganizationId
|
||||
)
|
||||
MERGE
|
||||
[dbo].[CollectionUser] as [Target]
|
||||
USING
|
||||
[NewCollectionUsers] AS [Source]
|
||||
ON
|
||||
[Target].[CollectionId] = [Source].[CollectionId]
|
||||
AND [Target].[OrganizationUserId] = [Source].[OrganizationUserId]
|
||||
-- Update the target if any values are different from the source
|
||||
WHEN MATCHED AND EXISTS(
|
||||
SELECT [Source].[ReadOnly], [Source].[HidePasswords], [Source].[Manage]
|
||||
EXCEPT
|
||||
SELECT [Target].[ReadOnly], [Target].[HidePasswords], [Target].[Manage]
|
||||
) THEN UPDATE SET
|
||||
[Target].[ReadOnly] = [Source].[ReadOnly],
|
||||
[Target].[HidePasswords] = [Source].[HidePasswords],
|
||||
[Target].[Manage] = [Source].[Manage]
|
||||
WHEN NOT MATCHED BY TARGET
|
||||
THEN INSERT VALUES
|
||||
(
|
||||
[Source].[CollectionId],
|
||||
[Source].[OrganizationUserId],
|
||||
[Source].[ReadOnly],
|
||||
[Source].[HidePasswords],
|
||||
[Source].[Manage]
|
||||
);
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionIds] @CollectionIds, @OrganizationId
|
||||
END
|
||||
GO
|
Reference in New Issue
Block a user