diff --git a/src/Api/AdminConsole/Public/Controllers/MembersController.cs b/src/Api/AdminConsole/Public/Controllers/MembersController.cs index 7bfe5648b649..3b2e82121dc1 100644 --- a/src/Api/AdminConsole/Public/Controllers/MembersController.cs +++ b/src/Api/AdminConsole/Public/Controllers/MembersController.cs @@ -1,7 +1,4 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using System.Net; +using System.Net; using Bit.Api.AdminConsole.Public.Models.Request; using Bit.Api.AdminConsole.Public.Models.Response; using Bit.Api.Models.Public.Response; @@ -24,11 +21,9 @@ public class MembersController : Controller private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IGroupRepository _groupRepository; private readonly IOrganizationService _organizationService; - private readonly IUserService _userService; private readonly ICurrentContext _currentContext; private readonly IUpdateOrganizationUserCommand _updateOrganizationUserCommand; private readonly IUpdateOrganizationUserGroupsCommand _updateOrganizationUserGroupsCommand; - private readonly IApplicationCacheService _applicationCacheService; private readonly IPaymentService _paymentService; private readonly IOrganizationRepository _organizationRepository; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; @@ -39,11 +34,9 @@ public MembersController( IOrganizationUserRepository organizationUserRepository, IGroupRepository groupRepository, IOrganizationService organizationService, - IUserService userService, ICurrentContext currentContext, IUpdateOrganizationUserCommand updateOrganizationUserCommand, IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand, - IApplicationCacheService applicationCacheService, IPaymentService paymentService, IOrganizationRepository organizationRepository, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, @@ -53,11 +46,9 @@ public MembersController( _organizationUserRepository = organizationUserRepository; _groupRepository = groupRepository; _organizationService = organizationService; - _userService = userService; _currentContext = currentContext; _updateOrganizationUserCommand = updateOrganizationUserCommand; _updateOrganizationUserGroupsCommand = updateOrganizationUserGroupsCommand; - _applicationCacheService = applicationCacheService; _paymentService = paymentService; _organizationRepository = organizationRepository; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; @@ -115,19 +106,18 @@ public async Task GetGroupIds(Guid id) /// /// /// Returns a list of your organization's members. - /// Member objects listed in this call do not include information about their associated collections. + /// Member objects listed in this call include information about their associated collections. /// [HttpGet] [ProducesResponseType(typeof(ListResponseModel), (int)HttpStatusCode.OK)] public async Task List() { - var organizationUserUserDetails = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_currentContext.OrganizationId.Value); - // TODO: Get all CollectionUser associations for the organization and marry them up here for the response. + var organizationUserUserDetails = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_currentContext.OrganizationId!.Value, includeCollections: true); var orgUsersTwoFactorIsEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUserUserDetails); var memberResponses = organizationUserUserDetails.Select(u => { - return new MemberResponseModel(u, orgUsersTwoFactorIsEnabled.FirstOrDefault(tuple => tuple.user == u).twoFactorIsEnabled, null); + return new MemberResponseModel(u, orgUsersTwoFactorIsEnabled.FirstOrDefault(tuple => tuple.user == u).twoFactorIsEnabled, u.Collections); }); var response = new ListResponseModel(memberResponses); return new JsonResult(response); @@ -158,7 +148,7 @@ public async Task Post([FromBody] MemberCreateRequestModel model) invite.AccessSecretsManager = hasStandaloneSecretsManager; - var user = await _organizationService.InviteUserAsync(_currentContext.OrganizationId.Value, null, + var user = await _organizationService.InviteUserAsync(_currentContext.OrganizationId!.Value, null, systemUser: null, invite, model.ExternalId); var response = new MemberResponseModel(user, invite.Collections); return new JsonResult(response); @@ -188,12 +178,12 @@ public async Task Put(Guid id, [FromBody] MemberUpdateRequestMode var updatedUser = model.ToOrganizationUser(existingUser); var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection()).ToList(); await _updateOrganizationUserCommand.UpdateUserAsync(updatedUser, existingUserType, null, associations, model.Groups); - MemberResponseModel response = null; + MemberResponseModel response; if (existingUser.UserId.HasValue) { var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id); - response = new MemberResponseModel(existingUserDetails, - await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(existingUserDetails), associations); + response = new MemberResponseModel(existingUserDetails!, + await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(existingUserDetails!), associations); } else { @@ -242,7 +232,7 @@ public async Task Remove(Guid id) { return new NotFoundResult(); } - await _removeOrganizationUserCommand.RemoveUserAsync(_currentContext.OrganizationId.Value, id, null); + await _removeOrganizationUserCommand.RemoveUserAsync(_currentContext.OrganizationId!.Value, id, null); return new OkResult(); } @@ -264,7 +254,7 @@ public async Task PostReinvite(Guid id) { return new NotFoundResult(); } - await _resendOrganizationInviteCommand.ResendInviteAsync(_currentContext.OrganizationId.Value, null, id); + await _resendOrganizationInviteCommand.ResendInviteAsync(_currentContext.OrganizationId!.Value, null, id); return new OkResult(); } } diff --git a/src/Api/AdminConsole/Public/Models/Response/AssociationWithPermissionsResponseModel.cs b/src/Api/AdminConsole/Public/Models/Response/AssociationWithPermissionsResponseModel.cs index e319ead8a48a..5ff12a220105 100644 --- a/src/Api/AdminConsole/Public/Models/Response/AssociationWithPermissionsResponseModel.cs +++ b/src/Api/AdminConsole/Public/Models/Response/AssociationWithPermissionsResponseModel.cs @@ -1,9 +1,15 @@ -using Bit.Core.Models.Data; +using System.Text.Json.Serialization; +using Bit.Core.Models.Data; namespace Bit.Api.AdminConsole.Public.Models.Response; public class AssociationWithPermissionsResponseModel : AssociationWithPermissionsBaseModel { + [JsonConstructor] + public AssociationWithPermissionsResponseModel() : base() + { + } + public AssociationWithPermissionsResponseModel(CollectionAccessSelection selection) { if (selection == null) diff --git a/test/Api.IntegrationTest/AdminConsole/Public/Controllers/MembersControllerTests.cs b/test/Api.IntegrationTest/AdminConsole/Public/Controllers/MembersControllerTests.cs index 11c60ad57c8b..2eeba5d47e3f 100644 --- a/test/Api.IntegrationTest/AdminConsole/Public/Controllers/MembersControllerTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Public/Controllers/MembersControllerTests.cs @@ -64,6 +64,17 @@ public async Task List_Member_Success() var (userEmail4, orgUser4) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.Admin); + var collection1 = await OrganizationTestHelpers.CreateCollectionAsync(_factory, _organization.Id, "Test Collection 1", users: + [ + new CollectionAccessSelection { Id = orgUser1.Id, ReadOnly = false, HidePasswords = false, Manage = true }, + new CollectionAccessSelection { Id = orgUser3.Id, ReadOnly = true, HidePasswords = false, Manage = false } + ]); + + var collection2 = await OrganizationTestHelpers.CreateCollectionAsync(_factory, _organization.Id, "Test Collection 2", users: + [ + new CollectionAccessSelection { Id = orgUser1.Id, ReadOnly = false, HidePasswords = true, Manage = false } + ]); + var response = await _client.GetAsync($"/public/members"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var result = await response.Content.ReadFromJsonAsync>(); @@ -71,23 +82,47 @@ public async Task List_Member_Success() Assert.Equal(5, result.Data.Count()); // The owner - Assert.NotNull(result.Data.SingleOrDefault(m => - m.Email == _ownerEmail && m.Type == OrganizationUserType.Owner)); + var ownerResult = result.Data.SingleOrDefault(m => m.Email == _ownerEmail && m.Type == OrganizationUserType.Owner); + Assert.NotNull(ownerResult); + Assert.Empty(ownerResult.Collections); - // The custom user + // The custom user with collections var user1Result = result.Data.Single(m => m.Email == userEmail1); Assert.Equal(OrganizationUserType.Custom, user1Result.Type); AssertHelper.AssertPropertyEqual( new PermissionsModel { AccessImportExport = true, ManagePolicies = true, AccessReports = true }, user1Result.Permissions); - - // Everyone else - Assert.NotNull(result.Data.SingleOrDefault(m => - m.Email == userEmail2 && m.Type == OrganizationUserType.Owner)); - Assert.NotNull(result.Data.SingleOrDefault(m => - m.Email == userEmail3 && m.Type == OrganizationUserType.User)); - Assert.NotNull(result.Data.SingleOrDefault(m => - m.Email == userEmail4 && m.Type == OrganizationUserType.Admin)); + // Verify collections + Assert.NotNull(user1Result.Collections); + Assert.Equal(2, user1Result.Collections.Count()); + var user1Collection1 = user1Result.Collections.Single(c => c.Id == collection1.Id); + Assert.False(user1Collection1.ReadOnly); + Assert.False(user1Collection1.HidePasswords); + Assert.True(user1Collection1.Manage); + var user1Collection2 = user1Result.Collections.Single(c => c.Id == collection2.Id); + Assert.False(user1Collection2.ReadOnly); + Assert.True(user1Collection2.HidePasswords); + Assert.False(user1Collection2.Manage); + + // The other owner + var user2Result = result.Data.SingleOrDefault(m => m.Email == userEmail2 && m.Type == OrganizationUserType.Owner); + Assert.NotNull(user2Result); + Assert.Empty(user2Result.Collections); + + // The user with one collection + var user3Result = result.Data.SingleOrDefault(m => m.Email == userEmail3 && m.Type == OrganizationUserType.User); + Assert.NotNull(user3Result); + Assert.NotNull(user3Result.Collections); + Assert.Single(user3Result.Collections); + var user3Collection1 = user3Result.Collections.Single(c => c.Id == collection1.Id); + Assert.True(user3Collection1.ReadOnly); + Assert.False(user3Collection1.HidePasswords); + Assert.False(user3Collection1.Manage); + + // The admin with no collections + var user4Result = result.Data.SingleOrDefault(m => m.Email == userEmail4 && m.Type == OrganizationUserType.Admin); + Assert.NotNull(user4Result); + Assert.Empty(user4Result.Collections); } [Fact] diff --git a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs index 3cd73c4b1c2c..c23ebff73686 100644 --- a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs +++ b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs @@ -151,6 +151,28 @@ public static async Task CreateGroup(ApiApplicationFactory factory, Guid return group; } + /// + /// Creates a collection with optional user and group associations. + /// + public static async Task CreateCollectionAsync( + ApiApplicationFactory factory, + Guid organizationId, + string name, + IEnumerable? users = null, + IEnumerable? groups = null) + { + var collectionRepository = factory.GetService(); + var collection = new Collection + { + OrganizationId = organizationId, + Name = name, + Type = CollectionType.SharedCollection + }; + + await collectionRepository.CreateAsync(collection, groups, users); + return collection; + } + /// /// Enables the Organization Data Ownership policy for the specified organization. ///