diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index f19fdc031..a478f6e78 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -4,6 +4,7 @@ using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; using Bit.Api.Utilities; using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers; +using Bit.Core; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; @@ -41,6 +42,7 @@ public class OrganizationUsersController : Controller private readonly IAcceptOrgUserCommand _acceptOrgUserCommand; private readonly IAuthorizationService _authorizationService; private readonly IApplicationCacheService _applicationCacheService; + private readonly IFeatureService _featureService; public OrganizationUsersController( IOrganizationRepository organizationRepository, @@ -56,7 +58,8 @@ public class OrganizationUsersController : Controller IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand, IAcceptOrgUserCommand acceptOrgUserCommand, IAuthorizationService authorizationService, - IApplicationCacheService applicationCacheService) + IApplicationCacheService applicationCacheService, + IFeatureService featureService) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -72,6 +75,7 @@ public class OrganizationUsersController : Controller _acceptOrgUserCommand = acceptOrgUserCommand; _authorizationService = authorizationService; _applicationCacheService = applicationCacheService; + _featureService = featureService; } [HttpGet("{id}")] @@ -305,43 +309,34 @@ public class OrganizationUsersController : Controller [HttpPut("{id}")] [HttpPost("{id}")] - public async Task Put(string orgId, string id, [FromBody] OrganizationUserUpdateRequestModel model) + public async Task Put(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model) { - var orgGuidId = new Guid(orgId); - if (!await _currentContext.ManageUsers(orgGuidId)) + if (!await _currentContext.ManageUsers(orgId)) { throw new NotFoundException(); } - var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id)); - if (organizationUser == null || organizationUser.OrganizationId != orgGuidId) + var organizationUser = await _organizationUserRepository.GetByIdAsync(id); + if (organizationUser == null || organizationUser.OrganizationId != orgId) { throw new NotFoundException(); } - var userId = _userService.GetProperUserId(User); - await _organizationService.SaveUserAsync(model.ToOrganizationUser(organizationUser), userId.Value, - model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), model.Groups); - } + // If admins are not allowed access to all collections, you cannot add yourself to a group + // In this case we just don't update groups + var userId = _userService.GetProperUserId(User).Value; + var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId); + var restrictEditingGroups = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1) && + organizationAbility.FlexibleCollections && + userId == organizationUser.UserId && + !organizationAbility.AllowAdminAccessToAllCollectionItems; - [HttpPut("{id}/groups")] - [HttpPost("{id}/groups")] - public async Task PutGroups(string orgId, string id, [FromBody] OrganizationUserUpdateGroupsRequestModel model) - { - var orgGuidId = new Guid(orgId); - if (!await _currentContext.ManageUsers(orgGuidId)) - { - throw new NotFoundException(); - } + var groups = restrictEditingGroups + ? null + : model.Groups; - var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id)); - if (organizationUser == null || organizationUser.OrganizationId != orgGuidId) - { - throw new NotFoundException(); - } - - var loggedInUserId = _userService.GetProperUserId(User); - await _updateOrganizationUserGroupsCommand.UpdateUserGroupsAsync(organizationUser, model.GroupIds.Select(g => new Guid(g)), loggedInUserId); + await _organizationService.SaveUserAsync(model.ToOrganizationUser(organizationUser), userId, + model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), groups); } [HttpPut("{userId}/reset-password-enrollment")] diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUserRequestModels.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUserRequestModels.cs index 6965b37fd..eacb3f9c0 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUserRequestModels.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUserRequestModels.cs @@ -102,12 +102,6 @@ public class OrganizationUserUpdateRequestModel } } -public class OrganizationUserUpdateGroupsRequestModel -{ - [Required] - public IEnumerable GroupIds { get; set; } -} - public class OrganizationUserResetPasswordEnrollmentRequestModel { public string ResetPasswordKey { get; set; } diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs index baf23bb9c..5af1a8619 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs @@ -1,10 +1,15 @@ -using Bit.Api.AdminConsole.Controllers; +using System.Security.Claims; +using Bit.Api.AdminConsole.Controllers; using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Context; using Bit.Core.Entities; +using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations; using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -94,4 +99,106 @@ public class OrganizationUsersControllerTests await sutProvider.GetDependency().Received(1) .UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id); } + + [Theory] + [BitAutoData] + public async Task Put_Success(OrganizationUserUpdateRequestModel model, + OrganizationUser organizationUser, OrganizationAbility organizationAbility, + SutProvider sutProvider, Guid savingUserId) + { + var orgId = organizationAbility.Id = organizationUser.OrganizationId; + sutProvider.GetDependency().ManageUsers(orgId).Returns(true); + sutProvider.GetDependency().GetByIdAsync(organizationUser.Id).Returns(organizationUser); + sutProvider.GetDependency().GetOrganizationAbilityAsync(orgId) + .Returns(organizationAbility); + sutProvider.GetDependency().GetProperUserId(Arg.Any()).Returns(savingUserId); + + var orgUserId = organizationUser.Id; + var orgUserEmail = organizationUser.Email; + + await sutProvider.Sut.Put(orgId, organizationUser.Id, model); + + sutProvider.GetDependency().Received(1).SaveUserAsync(Arg.Is(ou => + ou.Type == model.Type && + ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) && + ou.AccessSecretsManager == model.AccessSecretsManager && + ou.Id == orgUserId && + ou.Email == orgUserEmail), + savingUserId, + Arg.Is>(cas => + cas.All(c => model.Collections.Any(m => m.Id == c.Id))), + model.Groups); + } + + [Theory] + [BitAutoData] + public async Task Put_UpdateSelf_WithoutAllowAdminAccessToAllCollectionItems_DoesNotUpdateGroups(OrganizationUserUpdateRequestModel model, + OrganizationUser organizationUser, OrganizationAbility organizationAbility, + SutProvider sutProvider, Guid savingUserId) + { + // Updating self + organizationUser.UserId = savingUserId; + organizationAbility.FlexibleCollections = true; + organizationAbility.AllowAdminAccessToAllCollectionItems = false; + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true); + + var orgId = organizationAbility.Id = organizationUser.OrganizationId; + sutProvider.GetDependency().ManageUsers(orgId).Returns(true); + sutProvider.GetDependency().GetByIdAsync(organizationUser.Id).Returns(organizationUser); + sutProvider.GetDependency().GetOrganizationAbilityAsync(orgId) + .Returns(organizationAbility); + sutProvider.GetDependency().GetProperUserId(Arg.Any()).Returns(savingUserId); + + var orgUserId = organizationUser.Id; + var orgUserEmail = organizationUser.Email; + + await sutProvider.Sut.Put(orgId, organizationUser.Id, model); + + sutProvider.GetDependency().Received(1).SaveUserAsync(Arg.Is(ou => + ou.Type == model.Type && + ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) && + ou.AccessSecretsManager == model.AccessSecretsManager && + ou.Id == orgUserId && + ou.Email == orgUserEmail), + savingUserId, + Arg.Is>(cas => + cas.All(c => model.Collections.Any(m => m.Id == c.Id))), + null); + } + + [Theory] + [BitAutoData] + public async Task Put_UpdateSelf_WithAllowAdminAccessToAllCollectionItems_DoesUpdateGroups(OrganizationUserUpdateRequestModel model, + OrganizationUser organizationUser, OrganizationAbility organizationAbility, + SutProvider sutProvider, Guid savingUserId) + { + // Updating self + organizationUser.UserId = savingUserId; + organizationAbility.FlexibleCollections = true; + organizationAbility.AllowAdminAccessToAllCollectionItems = true; + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1).Returns(true); + + var orgId = organizationAbility.Id = organizationUser.OrganizationId; + sutProvider.GetDependency().ManageUsers(orgId).Returns(true); + sutProvider.GetDependency().GetByIdAsync(organizationUser.Id).Returns(organizationUser); + sutProvider.GetDependency().GetOrganizationAbilityAsync(orgId) + .Returns(organizationAbility); + sutProvider.GetDependency().GetProperUserId(Arg.Any()).Returns(savingUserId); + + var orgUserId = organizationUser.Id; + var orgUserEmail = organizationUser.Email; + + await sutProvider.Sut.Put(orgId, organizationUser.Id, model); + + sutProvider.GetDependency().Received(1).SaveUserAsync(Arg.Is(ou => + ou.Type == model.Type && + ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) && + ou.AccessSecretsManager == model.AccessSecretsManager && + ou.Id == orgUserId && + ou.Email == orgUserEmail), + savingUserId, + Arg.Is>(cas => + cas.All(c => model.Collections.Any(m => m.Id == c.Id))), + model.Groups); + } }