1
0
mirror of https://github.com/bitwarden/server.git synced 2025-02-01 23:31:41 +01:00

[AC-2172] Member modal - limit admin access (#3934)

* update OrganizationUsersController PUT and POST
* enforces new collection access checks when updating members
* refactor BulkCollectionAuthorizationHandler to avoid repeated db calls
This commit is contained in:
Thomas Rittson 2024-04-29 11:02:06 +10:00 committed by GitHub
parent 8142ba7bf2
commit ba36b2d26a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 463 additions and 56 deletions

View File

@ -3,6 +3,7 @@ using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Request.Organizations;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.Utilities; using Bit.Api.Utilities;
using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers; using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers;
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
@ -186,16 +187,28 @@ public class OrganizationUsersController : Controller
} }
[HttpPost("invite")] [HttpPost("invite")]
public async Task Invite(string orgId, [FromBody] OrganizationUserInviteRequestModel model) public async Task Invite(Guid orgId, [FromBody] OrganizationUserInviteRequestModel model)
{ {
var orgGuidId = new Guid(orgId); if (!await _currentContext.ManageUsers(orgId))
if (!await _currentContext.ManageUsers(orgGuidId))
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
// Flexible Collections - check the user has permission to grant access to the collections for the new user
if (await FlexibleCollectionsIsEnabledAsync(orgId) && _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1))
{
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Collections.Select(a => a.Id));
var authorized =
(await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.ModifyAccess))
.Succeeded;
if (!authorized)
{
throw new NotFoundException("You are not authorized to grant access to these collections.");
}
}
var userId = _userService.GetProperUserId(User); var userId = _userService.GetProperUserId(User);
var result = await _organizationService.InviteUsersAsync(orgGuidId, userId.Value, await _organizationService.InviteUsersAsync(orgId, userId.Value,
new (OrganizationUserInvite, string)[] { (new OrganizationUserInvite(model.ToData()), null) }); new (OrganizationUserInvite, string)[] { (new OrganizationUserInvite(model.ToData()), null) });
} }
@ -316,6 +329,35 @@ public class OrganizationUsersController : Controller
[HttpPut("{id}")] [HttpPut("{id}")]
[HttpPost("{id}")] [HttpPost("{id}")]
public async Task Put(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model) public async Task Put(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model)
{
if (await FlexibleCollectionsIsEnabledAsync(orgId) && _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1))
{
// Use new Flexible Collections v1 logic
await Put_vNext(orgId, id, model);
return;
}
// Pre-Flexible Collections v1 code follows
if (!await _currentContext.ManageUsers(orgId))
{
throw new NotFoundException();
}
var organizationUser = await _organizationUserRepository.GetByIdAsync(id);
if (organizationUser == null || organizationUser.OrganizationId != orgId)
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User);
await _updateOrganizationUserCommand.UpdateUserAsync(model.ToOrganizationUser(organizationUser), userId.Value,
model.Collections.Select(c => c.ToSelectionReadOnly()).ToList(), model.Groups);
}
/// <summary>
/// Put logic for Flexible Collections v1
/// </summary>
private async Task Put_vNext(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model)
{ {
if (!await _currentContext.ManageUsers(orgId)) if (!await _currentContext.ManageUsers(orgId))
{ {
@ -332,17 +374,44 @@ public class OrganizationUsersController : Controller
// In this case we just don't update groups // In this case we just don't update groups
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId); var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId);
var restrictEditingGroups = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1) && var editingSelf = userId == organizationUser.UserId;
organizationAbility.FlexibleCollections &&
userId == organizationUser.UserId &&
!organizationAbility.AllowAdminAccessToAllCollectionItems;
var groups = restrictEditingGroups var groups = editingSelf && !organizationAbility.AllowAdminAccessToAllCollectionItems
? null ? null
: model.Groups; : model.Groups;
// The client only sends collections that the saving user has permissions to edit.
// On the server side, we need to (1) confirm this and (2) concat these with the collections that the user
// can't edit before saving to the database.
var (_, currentAccess) = await _organizationUserRepository.GetByIdWithCollectionsAsync(id);
var currentCollections = await _collectionRepository
.GetManyByManyIdsAsync(currentAccess.Select(cas => cas.Id));
var readonlyCollectionIds = new HashSet<Guid>();
foreach (var collection in currentCollections)
{
if (!(await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyAccess))
.Succeeded)
{
readonlyCollectionIds.Add(collection.Id);
}
}
if (model.Collections.Any(c => readonlyCollectionIds.Contains(c.Id)))
{
throw new BadRequestException("You must have Can Manage permissions to edit a collection's membership");
}
var editedCollectionAccess = model.Collections
.Select(c => c.ToSelectionReadOnly());
var readonlyCollectionAccess = currentAccess
.Where(ca => readonlyCollectionIds.Contains(ca.Id));
var collectionsToSave = editedCollectionAccess
.Concat(readonlyCollectionAccess)
.ToList();
await _updateOrganizationUserCommand.UpdateUserAsync(model.ToOrganizationUser(organizationUser), userId, await _updateOrganizationUserCommand.UpdateUserAsync(model.ToOrganizationUser(organizationUser), userId,
model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), groups); collectionsToSave, groups);
} }
[HttpPut("{userId}/reset-password-enrollment")] [HttpPut("{userId}/reset-password-enrollment")]

View File

@ -1,4 +1,5 @@
#nullable enable #nullable enable
using Bit.Core;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -20,16 +21,20 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly ICollectionRepository _collectionRepository; private readonly ICollectionRepository _collectionRepository;
private readonly IApplicationCacheService _applicationCacheService; private readonly IApplicationCacheService _applicationCacheService;
private readonly IFeatureService _featureService;
private Guid _targetOrganizationId; private Guid _targetOrganizationId;
private HashSet<Guid>? _managedCollectionsIds;
public BulkCollectionAuthorizationHandler( public BulkCollectionAuthorizationHandler(
ICurrentContext currentContext, ICurrentContext currentContext,
ICollectionRepository collectionRepository, ICollectionRepository collectionRepository,
IApplicationCacheService applicationCacheService) IApplicationCacheService applicationCacheService,
IFeatureService featureService)
{ {
_currentContext = currentContext; _currentContext = currentContext;
_collectionRepository = collectionRepository; _collectionRepository = collectionRepository;
_applicationCacheService = applicationCacheService; _applicationCacheService = applicationCacheService;
_featureService = featureService;
} }
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
@ -129,7 +134,7 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
// ensure they have access for the collection being read // ensure they have access for the collection being read
if (org is not null) if (org is not null)
{ {
var canManageCollections = await CanManageCollectionsAsync(resources, org); var canManageCollections = await CanManageCollectionsAsync(resources);
if (canManageCollections) if (canManageCollections)
{ {
context.Succeed(requirement); context.Succeed(requirement);
@ -162,7 +167,7 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
// ensure they have access with manage permission for the collection being read // ensure they have access with manage permission for the collection being read
if (org is not null) if (org is not null)
{ {
var canManageCollections = await CanManageCollectionsAsync(resources, org); var canManageCollections = await CanManageCollectionsAsync(resources);
if (canManageCollections) if (canManageCollections)
{ {
context.Succeed(requirement); context.Succeed(requirement);
@ -184,10 +189,19 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
IAuthorizationRequirement requirement, ICollection<Collection> resources, IAuthorizationRequirement requirement, ICollection<Collection> resources,
CurrentContextOrganization? org) CurrentContextOrganization? org)
{ {
// Owners, Admins, and users with EditAnyCollection permission can always manage collection access // Users with EditAnyCollection permission can always update a collection
if (org is if (org is
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true })
{ Permissions.EditAnyCollection: true }) {
context.Succeed(requirement);
return;
}
// If V1 is enabled, Owners and Admins can update any collection only if permitted by collection management settings
var organizationAbility = await GetOrganizationAbilityAsync(org);
var allowAdminAccessToAllCollectionItems = !_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1) ||
organizationAbility is { AllowAdminAccessToAllCollectionItems: true };
if (allowAdminAccessToAllCollectionItems && org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin })
{ {
context.Succeed(requirement); context.Succeed(requirement);
return; return;
@ -197,7 +211,7 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
// ensure they have manage permission for the collection being managed // ensure they have manage permission for the collection being managed
if (org is not null) if (org is not null)
{ {
var canManageCollections = await CanManageCollectionsAsync(resources, org); var canManageCollections = await CanManageCollectionsAsync(resources);
if (canManageCollections) if (canManageCollections)
{ {
context.Succeed(requirement); context.Succeed(requirement);
@ -229,7 +243,7 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
// ensure acting user has manage permissions for all collections being deleted // ensure acting user has manage permissions for all collections being deleted
if (await GetOrganizationAbilityAsync(org) is { LimitCollectionCreationDeletion: false }) if (await GetOrganizationAbilityAsync(org) is { LimitCollectionCreationDeletion: false })
{ {
var canManageCollections = await CanManageCollectionsAsync(resources, org); var canManageCollections = await CanManageCollectionsAsync(resources);
if (canManageCollections) if (canManageCollections)
{ {
context.Succeed(requirement); context.Succeed(requirement);
@ -244,21 +258,19 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
} }
} }
private async Task<bool> CanManageCollectionsAsync( private async Task<bool> CanManageCollectionsAsync(ICollection<Collection> targetCollections)
ICollection<Collection> targetCollections,
CurrentContextOrganization org)
{ {
// List of collection Ids the acting user has access to if (_managedCollectionsIds == null)
var assignedCollectionIds = {
(await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId!.Value, useFlexibleCollections: true)) var allUserCollections = await _collectionRepository
.Where(c => .GetManyByUserIdAsync(_currentContext.UserId!.Value, useFlexibleCollections: true);
// Check Collections with Manage permission _managedCollectionsIds = allUserCollections
c.Manage && c.OrganizationId == org.Id) .Where(c => c.Manage)
.Select(c => c.Id) .Select(c => c.Id)
.ToHashSet(); .ToHashSet();
}
// Check if the acting user has access to all target collections return targetCollections.All(tc => _managedCollectionsIds.Contains(tc.Id));
return targetCollections.All(tc => assignedCollectionIds.Contains(tc.Id));
} }
private async Task<OrganizationAbility?> GetOrganizationAbilityAsync(CurrentContextOrganization? organization) private async Task<OrganizationAbility?> GetOrganizationAbilityAsync(CurrentContextOrganization? organization)

View File

@ -9,11 +9,12 @@ BEGIN
SELECT SELECT
CU.[CollectionId] Id, CU.[CollectionId] Id,
CU.[ReadOnly], CU.[ReadOnly],
CU.[HidePasswords] CU.[HidePasswords],
CU.[Manage]
FROM FROM
[dbo].[OrganizationUser] OU [dbo].[OrganizationUser] OU
INNER JOIN INNER JOIN
[dbo].[CollectionUser] CU ON OU.[AccessAll] = 0 AND CU.[OrganizationUserId] = [OU].[Id] [dbo].[CollectionUser] CU ON OU.[AccessAll] = 0 AND CU.[OrganizationUserId] = [OU].[Id]
WHERE WHERE
[OrganizationUserId] = @Id [OrganizationUserId] = @Id
END END

View File

@ -1,6 +1,8 @@
using System.Security.Claims; using System.Security.Claims;
using Bit.Api.AdminConsole.Controllers; using Bit.Api.AdminConsole.Controllers;
using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Models.Request;
using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
@ -10,6 +12,8 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
@ -104,23 +108,69 @@ public class OrganizationUsersControllerTests
.UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id); .UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id);
} }
[Theory]
[BitAutoData]
public async Task Invite_Success(OrganizationAbility organizationAbility, OrganizationUserInviteRequestModel model,
Guid userId, SutProvider<OrganizationUsersController> sutProvider)
{
organizationAbility.FlexibleCollections = true;
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationAbility.Id).Returns(true);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
.Returns(organizationAbility);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
Arg.Any<IEnumerable<Collection>>(),
Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyAccess)))
.Returns(AuthorizationResult.Success());
sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
await sutProvider.Sut.Invite(organizationAbility.Id, model);
await sutProvider.GetDependency<IOrganizationService>().Received(1).InviteUsersAsync(organizationAbility.Id,
userId, Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(invites =>
invites.Count() == 1 &&
invites.First().Item1.Emails.SequenceEqual(model.Emails) &&
invites.First().Item1.Type == model.Type &&
invites.First().Item1.AccessSecretsManager == model.AccessSecretsManager));
}
[Theory]
[BitAutoData]
public async Task Invite_NotAuthorizedToGiveAccessToCollections_Throws(OrganizationAbility organizationAbility, OrganizationUserInviteRequestModel model,
Guid userId, SutProvider<OrganizationUsersController> sutProvider)
{
organizationAbility.FlexibleCollections = true;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true);
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationAbility.Id).Returns(true);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
.Returns(organizationAbility);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
Arg.Any<IEnumerable<Collection>>(),
Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyAccess)))
.Returns(AuthorizationResult.Failed());
sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Invite(organizationAbility.Id, model));
Assert.Contains("You are not authorized", exception.Message);
}
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task Put_Success(OrganizationUserUpdateRequestModel model, public async Task Put_Success(OrganizationUserUpdateRequestModel model,
OrganizationUser organizationUser, OrganizationAbility organizationAbility, OrganizationUser organizationUser, OrganizationAbility organizationAbility,
SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId) SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)
{ {
var orgId = organizationAbility.Id = organizationUser.OrganizationId; organizationAbility.FlexibleCollections = false;
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true); sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(false);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(organizationUser.Id).Returns(organizationUser);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(orgId)
.Returns(organizationAbility);
sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(savingUserId);
Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, model, false);
// Save these for later - organizationUser object will be mutated
var orgUserId = organizationUser.Id; var orgUserId = organizationUser.Id;
var orgUserEmail = organizationUser.Email; var orgUserEmail = organizationUser.Email;
await sutProvider.Sut.Put(orgId, organizationUser.Id, model); await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);
await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou => await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>
ou.Type == model.Type && ou.Type == model.Type &&
@ -142,21 +192,16 @@ public class OrganizationUsersControllerTests
{ {
// Updating self // Updating self
organizationUser.UserId = savingUserId; organizationUser.UserId = savingUserId;
organizationAbility.FlexibleCollections = true;
organizationAbility.AllowAdminAccessToAllCollectionItems = false; organizationAbility.AllowAdminAccessToAllCollectionItems = false;
organizationAbility.FlexibleCollections = true;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true); sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true);
var orgId = organizationAbility.Id = organizationUser.OrganizationId; Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, model, true);
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(organizationUser.Id).Returns(organizationUser);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(orgId)
.Returns(organizationAbility);
sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(savingUserId);
var orgUserId = organizationUser.Id; var orgUserId = organizationUser.Id;
var orgUserEmail = organizationUser.Email; var orgUserEmail = organizationUser.Email;
await sutProvider.Sut.Put(orgId, organizationUser.Id, model); await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);
await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou => await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>
ou.Type == model.Type && ou.Type == model.Type &&
@ -182,17 +227,12 @@ public class OrganizationUsersControllerTests
organizationAbility.AllowAdminAccessToAllCollectionItems = true; organizationAbility.AllowAdminAccessToAllCollectionItems = true;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true); sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true);
var orgId = organizationAbility.Id = organizationUser.OrganizationId; Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, model, true);
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(organizationUser.Id).Returns(organizationUser);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(orgId)
.Returns(organizationAbility);
sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(savingUserId);
var orgUserId = organizationUser.Id; var orgUserId = organizationUser.Id;
var orgUserEmail = organizationUser.Email; var orgUserEmail = organizationUser.Email;
await sutProvider.Sut.Put(orgId, organizationUser.Id, model); await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);
await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou => await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>
ou.Type == model.Type && ou.Type == model.Type &&
@ -206,6 +246,124 @@ public class OrganizationUsersControllerTests
model.Groups); model.Groups);
} }
[Theory]
[BitAutoData]
public async Task Put_UpdateCollections_OnlyUpdatesCollectionsTheSavingUserCanUpdate(OrganizationUserUpdateRequestModel model,
OrganizationUser organizationUser, OrganizationAbility organizationAbility,
SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)
{
organizationAbility.FlexibleCollections = true;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true);
Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, model, false);
var editedCollectionId = CoreHelpers.GenerateComb();
var readonlyCollectionId1 = CoreHelpers.GenerateComb();
var readonlyCollectionId2 = CoreHelpers.GenerateComb();
var currentCollectionAccess = new List<CollectionAccessSelection>
{
new()
{
Id = editedCollectionId,
HidePasswords = true,
Manage = false,
ReadOnly = true
},
new()
{
Id = readonlyCollectionId1,
HidePasswords = false,
Manage = true,
ReadOnly = false
},
new()
{
Id = readonlyCollectionId2,
HidePasswords = false,
Manage = false,
ReadOnly = false
},
};
// User is upgrading editedCollectionId to manage
model.Collections = new List<SelectionReadOnlyRequestModel>
{
new() { Id = editedCollectionId, HidePasswords = false, Manage = true, ReadOnly = false }
};
// Save these for later - organizationUser object will be mutated
var orgUserId = organizationUser.Id;
var orgUserEmail = organizationUser.Email;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdWithCollectionsAsync(organizationUser.Id)
.Returns(new Tuple<OrganizationUser, ICollection<CollectionAccessSelection>>(organizationUser,
currentCollectionAccess));
var currentCollections = currentCollectionAccess
.Select(cas => new Collection { Id = cas.Id }).ToList();
sutProvider.GetDependency<ICollectionRepository>()
.GetManyByManyIdsAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(currentCollections);
// Authorize the editedCollection
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Is<Collection>(c => c.Id == editedCollectionId),
Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyAccess)))
.Returns(AuthorizationResult.Success());
// Do not authorize the readonly collections
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Is<Collection>(c => c.Id == readonlyCollectionId1 || c.Id == readonlyCollectionId2),
Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyAccess)))
.Returns(AuthorizationResult.Failed());
await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);
// Expect all collection access (modified and unmodified) to be saved
await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>
ou.Type == model.Type &&
ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) &&
ou.AccessSecretsManager == model.AccessSecretsManager &&
ou.Id == orgUserId &&
ou.Email == orgUserEmail),
savingUserId,
Arg.Is<List<CollectionAccessSelection>>(cas =>
cas.Select(c => c.Id).SequenceEqual(currentCollectionAccess.Select(c => c.Id)) &&
cas.First(c => c.Id == editedCollectionId).Manage == true &&
cas.First(c => c.Id == editedCollectionId).ReadOnly == false &&
cas.First(c => c.Id == editedCollectionId).HidePasswords == false),
model.Groups);
}
[Theory]
[BitAutoData]
public async Task Put_UpdateCollections_ThrowsIfSavingUserCannotUpdateCollections(OrganizationUserUpdateRequestModel model,
OrganizationUser organizationUser, OrganizationAbility organizationAbility,
SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)
{
organizationAbility.FlexibleCollections = true;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true);
Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, model, false);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdWithCollectionsAsync(organizationUser.Id)
.Returns(new Tuple<OrganizationUser, ICollection<CollectionAccessSelection>>(organizationUser,
model.Collections.Select(cas => cas.ToSelectionReadOnly()).ToList()));
var collections = model.Collections.Select(cas => new Collection { Id = cas.Id }).ToList();
sutProvider.GetDependency<ICollectionRepository>()
.GetManyByManyIdsAsync(Arg.Is<IEnumerable<Guid>>(guids => guids.SequenceEqual(collections.Select(c => c.Id))))
.Returns(collections);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Is<Collection>(c => collections.Contains(c)),
Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyAccess)))
.Returns(AuthorizationResult.Failed());
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model));
Assert.Contains("You must have Can Manage permission", exception.Message);
}
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task Get_WithFlexibleCollections_ReturnsUsers( public async Task Get_WithFlexibleCollections_ReturnsUsers(
@ -283,6 +441,36 @@ public class OrganizationUsersControllerTests
Assert.False(customUserResponse.Permissions.DeleteAssignedCollections); Assert.False(customUserResponse.Permissions.DeleteAssignedCollections);
} }
private void Put_Setup(SutProvider<OrganizationUsersController> sutProvider, OrganizationAbility organizationAbility,
OrganizationUser organizationUser, Guid savingUserId, OrganizationUserUpdateRequestModel model, bool authorizeAll)
{
var orgId = organizationAbility.Id = organizationUser.OrganizationId;
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(organizationUser.Id).Returns(organizationUser);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(orgId)
.Returns(organizationAbility);
sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(savingUserId);
if (authorizeAll)
{
// Simple case: saving user can edit all collections, all collection access is replaced
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdWithCollectionsAsync(organizationUser.Id)
.Returns(new Tuple<OrganizationUser, ICollection<CollectionAccessSelection>>(organizationUser,
model.Collections.Select(cas => cas.ToSelectionReadOnly()).ToList()));
var collections = model.Collections.Select(cas => new Collection { Id = cas.Id }).ToList();
sutProvider.GetDependency<ICollectionRepository>()
.GetManyByManyIdsAsync(Arg.Is<IEnumerable<Guid>>(guids => guids.SequenceEqual(collections.Select(c => c.Id))))
.Returns(collections);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Is<Collection>(c => collections.Contains(c)),
Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.ModifyAccess)))
.Returns(AuthorizationResult.Success());
}
}
private void Get_Setup(OrganizationAbility organizationAbility, private void Get_Setup(OrganizationAbility organizationAbility,
ICollection<OrganizationUserUserDetails> organizationUsers, ICollection<OrganizationUserUserDetails> organizationUsers,
SutProvider<OrganizationUsersController> sutProvider) SutProvider<OrganizationUsersController> sutProvider)

View File

@ -1,5 +1,6 @@
using System.Security.Claims; using System.Security.Claims;
using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -536,7 +537,7 @@ public class BulkCollectionAuthorizationHandlerTests
[Theory, CollectionCustomization] [Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Owner)]
public async Task CanUpdateCollection_WhenAdminOrOwner_Success( public async Task CanUpdateCollection_WhenAdminOrOwner_WithoutV1Enabled_Success(
OrganizationUserType userType, OrganizationUserType userType,
Guid userId, SutProvider<BulkCollectionAuthorizationHandler> sutProvider, Guid userId, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<Collection> collections, ICollection<Collection> collections,
@ -569,6 +570,88 @@ public class BulkCollectionAuthorizationHandlerTests
} }
} }
[Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)]
public async Task CanUpdateCollection_WhenAdminOrOwner_WithV1Enabled_PermittedByCollectionManagementSettings_Success(
OrganizationUserType userType,
Guid userId, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<Collection> collections, CurrentContextOrganization organization,
OrganizationAbility organizationAbility)
{
organization.Type = userType;
organization.Permissions = new Permissions();
organizationAbility.Id = organization.Id;
organizationAbility.AllowAdminAccessToAllCollectionItems = true;
var operationsToTest = new[]
{
BulkCollectionOperations.Update, BulkCollectionOperations.ModifyAccess
};
foreach (var op in operationsToTest)
{
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id)
.Returns(organizationAbility);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true);
var context = new AuthorizationHandlerContext(
new[] { op },
new ClaimsPrincipal(),
collections);
await sutProvider.Sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
// Recreate the SUT to reset the mocks/dependencies between tests
sutProvider.Recreate();
}
}
[Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)]
public async Task CanUpdateCollection_WhenAdminOrOwner_WithV1Enabled_NotPermittedByCollectionManagementSettings_Failure(
OrganizationUserType userType,
Guid userId, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<Collection> collections, CurrentContextOrganization organization,
OrganizationAbility organizationAbility)
{
organization.Type = userType;
organization.Permissions = new Permissions();
organizationAbility.Id = organization.Id;
organizationAbility.AllowAdminAccessToAllCollectionItems = false;
var operationsToTest = new[]
{
BulkCollectionOperations.Update, BulkCollectionOperations.ModifyAccess
};
foreach (var op in operationsToTest)
{
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id)
.Returns(organizationAbility);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true);
var context = new AuthorizationHandlerContext(
new[] { op },
new ClaimsPrincipal(),
collections);
await sutProvider.Sut.HandleAsync(context);
Assert.False(context.HasSucceeded);
// Recreate the SUT to reset the mocks/dependencies between tests
sutProvider.Recreate();
}
}
[Theory, BitAutoData, CollectionCustomization] [Theory, BitAutoData, CollectionCustomization]
public async Task CanUpdateCollection_WithEditAnyCollectionPermission_Success( public async Task CanUpdateCollection_WithEditAnyCollectionPermission_Success(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
@ -968,6 +1051,39 @@ public class BulkCollectionAuthorizationHandlerTests
} }
} }
[Theory, BitAutoData, CollectionCustomization]
public async Task CachesCollectionsWithCanManagePermissions(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
CollectionDetails collection1, CollectionDetails collection2,
CurrentContextOrganization organization, Guid actingUserId)
{
organization.Type = OrganizationUserType.User;
organization.Permissions = new Permissions();
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>()
.GetManyByUserIdAsync(actingUserId, Arg.Any<bool>())
.Returns(new List<CollectionDetails>() { collection1, collection2 });
var context1 = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Update },
new ClaimsPrincipal(),
collection1);
await sutProvider.Sut.HandleAsync(context1);
var context2 = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Update },
new ClaimsPrincipal(),
collection2);
await sutProvider.Sut.HandleAsync(context2);
// Expect: only calls the database once
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdAsync(Arg.Any<Guid>(), Arg.Any<bool>());
}
private static void ArrangeOrganizationAbility( private static void ArrangeOrganizationAbility(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
CurrentContextOrganization organization, bool limitCollectionCreationDeletion) CurrentContextOrganization organization, bool limitCollectionCreationDeletion)

View File

@ -0,0 +1,21 @@
-- Update to add the Manage column
CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_ReadWithCollectionsById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
EXEC [OrganizationUser_ReadById] @Id
SELECT
CU.[CollectionId] Id,
CU.[ReadOnly],
CU.[HidePasswords],
CU.[Manage]
FROM
[dbo].[OrganizationUser] OU
INNER JOIN
[dbo].[CollectionUser] CU ON OU.[AccessAll] = 0 AND CU.[OrganizationUserId] = [OU].[Id]
WHERE
[OrganizationUserId] = @Id
END