Skip to content

Commit de56b7f

Browse files
authored
[PM-26099] Update public list members endpoint to include collections (#6503)
* Add CreateCollectionAsync method to OrganizationTestHelpers for collection creation with user and group associations * Update public MembersController List endpoint to include associated collections in member response model * Update MembersControllerTests to validate collection associations in List endpoint. Add JsonConstructor to AssociationWithPermissionsResponseModel * Refactor MembersController by removing unused IUserService and IApplicationCacheService dependencies. * Remove nullable disable directive from Public MembersController
1 parent 0ea9e2e commit de56b7f

File tree

4 files changed

+85
-32
lines changed

4 files changed

+85
-32
lines changed

src/Api/AdminConsole/Public/Controllers/MembersController.cs

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
// FIXME: Update this file to be null safe and then delete the line below
2-
#nullable disable
3-
4-
using System.Net;
1+
using System.Net;
52
using Bit.Api.AdminConsole.Public.Models.Request;
63
using Bit.Api.AdminConsole.Public.Models.Response;
74
using Bit.Api.Models.Public.Response;
@@ -24,11 +21,9 @@ public class MembersController : Controller
2421
private readonly IOrganizationUserRepository _organizationUserRepository;
2522
private readonly IGroupRepository _groupRepository;
2623
private readonly IOrganizationService _organizationService;
27-
private readonly IUserService _userService;
2824
private readonly ICurrentContext _currentContext;
2925
private readonly IUpdateOrganizationUserCommand _updateOrganizationUserCommand;
3026
private readonly IUpdateOrganizationUserGroupsCommand _updateOrganizationUserGroupsCommand;
31-
private readonly IApplicationCacheService _applicationCacheService;
3227
private readonly IPaymentService _paymentService;
3328
private readonly IOrganizationRepository _organizationRepository;
3429
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
@@ -39,11 +34,9 @@ public MembersController(
3934
IOrganizationUserRepository organizationUserRepository,
4035
IGroupRepository groupRepository,
4136
IOrganizationService organizationService,
42-
IUserService userService,
4337
ICurrentContext currentContext,
4438
IUpdateOrganizationUserCommand updateOrganizationUserCommand,
4539
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand,
46-
IApplicationCacheService applicationCacheService,
4740
IPaymentService paymentService,
4841
IOrganizationRepository organizationRepository,
4942
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
@@ -53,11 +46,9 @@ public MembersController(
5346
_organizationUserRepository = organizationUserRepository;
5447
_groupRepository = groupRepository;
5548
_organizationService = organizationService;
56-
_userService = userService;
5749
_currentContext = currentContext;
5850
_updateOrganizationUserCommand = updateOrganizationUserCommand;
5951
_updateOrganizationUserGroupsCommand = updateOrganizationUserGroupsCommand;
60-
_applicationCacheService = applicationCacheService;
6152
_paymentService = paymentService;
6253
_organizationRepository = organizationRepository;
6354
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
@@ -115,19 +106,18 @@ public async Task<IActionResult> GetGroupIds(Guid id)
115106
/// </summary>
116107
/// <remarks>
117108
/// Returns a list of your organization's members.
118-
/// Member objects listed in this call do not include information about their associated collections.
109+
/// Member objects listed in this call include information about their associated collections.
119110
/// </remarks>
120111
[HttpGet]
121112
[ProducesResponseType(typeof(ListResponseModel<MemberResponseModel>), (int)HttpStatusCode.OK)]
122113
public async Task<IActionResult> List()
123114
{
124-
var organizationUserUserDetails = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_currentContext.OrganizationId.Value);
125-
// TODO: Get all CollectionUser associations for the organization and marry them up here for the response.
115+
var organizationUserUserDetails = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_currentContext.OrganizationId!.Value, includeCollections: true);
126116

127117
var orgUsersTwoFactorIsEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUserUserDetails);
128118
var memberResponses = organizationUserUserDetails.Select(u =>
129119
{
130-
return new MemberResponseModel(u, orgUsersTwoFactorIsEnabled.FirstOrDefault(tuple => tuple.user == u).twoFactorIsEnabled, null);
120+
return new MemberResponseModel(u, orgUsersTwoFactorIsEnabled.FirstOrDefault(tuple => tuple.user == u).twoFactorIsEnabled, u.Collections);
131121
});
132122
var response = new ListResponseModel<MemberResponseModel>(memberResponses);
133123
return new JsonResult(response);
@@ -158,7 +148,7 @@ public async Task<IActionResult> Post([FromBody] MemberCreateRequestModel model)
158148

159149
invite.AccessSecretsManager = hasStandaloneSecretsManager;
160150

161-
var user = await _organizationService.InviteUserAsync(_currentContext.OrganizationId.Value, null,
151+
var user = await _organizationService.InviteUserAsync(_currentContext.OrganizationId!.Value, null,
162152
systemUser: null, invite, model.ExternalId);
163153
var response = new MemberResponseModel(user, invite.Collections);
164154
return new JsonResult(response);
@@ -188,12 +178,12 @@ public async Task<IActionResult> Put(Guid id, [FromBody] MemberUpdateRequestMode
188178
var updatedUser = model.ToOrganizationUser(existingUser);
189179
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection()).ToList();
190180
await _updateOrganizationUserCommand.UpdateUserAsync(updatedUser, existingUserType, null, associations, model.Groups);
191-
MemberResponseModel response = null;
181+
MemberResponseModel response;
192182
if (existingUser.UserId.HasValue)
193183
{
194184
var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id);
195-
response = new MemberResponseModel(existingUserDetails,
196-
await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(existingUserDetails), associations);
185+
response = new MemberResponseModel(existingUserDetails!,
186+
await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(existingUserDetails!), associations);
197187
}
198188
else
199189
{
@@ -242,7 +232,7 @@ public async Task<IActionResult> Remove(Guid id)
242232
{
243233
return new NotFoundResult();
244234
}
245-
await _removeOrganizationUserCommand.RemoveUserAsync(_currentContext.OrganizationId.Value, id, null);
235+
await _removeOrganizationUserCommand.RemoveUserAsync(_currentContext.OrganizationId!.Value, id, null);
246236
return new OkResult();
247237
}
248238

@@ -264,7 +254,7 @@ public async Task<IActionResult> PostReinvite(Guid id)
264254
{
265255
return new NotFoundResult();
266256
}
267-
await _resendOrganizationInviteCommand.ResendInviteAsync(_currentContext.OrganizationId.Value, null, id);
257+
await _resendOrganizationInviteCommand.ResendInviteAsync(_currentContext.OrganizationId!.Value, null, id);
268258
return new OkResult();
269259
}
270260
}

src/Api/AdminConsole/Public/Models/Response/AssociationWithPermissionsResponseModel.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
using Bit.Core.Models.Data;
1+
using System.Text.Json.Serialization;
2+
using Bit.Core.Models.Data;
23

34
namespace Bit.Api.AdminConsole.Public.Models.Response;
45

56
public class AssociationWithPermissionsResponseModel : AssociationWithPermissionsBaseModel
67
{
8+
[JsonConstructor]
9+
public AssociationWithPermissionsResponseModel() : base()
10+
{
11+
}
12+
713
public AssociationWithPermissionsResponseModel(CollectionAccessSelection selection)
814
{
915
if (selection == null)

test/Api.IntegrationTest/AdminConsole/Public/Controllers/MembersControllerTests.cs

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,30 +64,65 @@ public async Task List_Member_Success()
6464
var (userEmail4, orgUser4) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
6565
OrganizationUserType.Admin);
6666

67+
var collection1 = await OrganizationTestHelpers.CreateCollectionAsync(_factory, _organization.Id, "Test Collection 1", users:
68+
[
69+
new CollectionAccessSelection { Id = orgUser1.Id, ReadOnly = false, HidePasswords = false, Manage = true },
70+
new CollectionAccessSelection { Id = orgUser3.Id, ReadOnly = true, HidePasswords = false, Manage = false }
71+
]);
72+
73+
var collection2 = await OrganizationTestHelpers.CreateCollectionAsync(_factory, _organization.Id, "Test Collection 2", users:
74+
[
75+
new CollectionAccessSelection { Id = orgUser1.Id, ReadOnly = false, HidePasswords = true, Manage = false }
76+
]);
77+
6778
var response = await _client.GetAsync($"/public/members");
6879
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
6980
var result = await response.Content.ReadFromJsonAsync<ListResponseModel<MemberResponseModel>>();
7081
Assert.NotNull(result?.Data);
7182
Assert.Equal(5, result.Data.Count());
7283

7384
// The owner
74-
Assert.NotNull(result.Data.SingleOrDefault(m =>
75-
m.Email == _ownerEmail && m.Type == OrganizationUserType.Owner));
85+
var ownerResult = result.Data.SingleOrDefault(m => m.Email == _ownerEmail && m.Type == OrganizationUserType.Owner);
86+
Assert.NotNull(ownerResult);
87+
Assert.Empty(ownerResult.Collections);
7688

77-
// The custom user
89+
// The custom user with collections
7890
var user1Result = result.Data.Single(m => m.Email == userEmail1);
7991
Assert.Equal(OrganizationUserType.Custom, user1Result.Type);
8092
AssertHelper.AssertPropertyEqual(
8193
new PermissionsModel { AccessImportExport = true, ManagePolicies = true, AccessReports = true },
8294
user1Result.Permissions);
83-
84-
// Everyone else
85-
Assert.NotNull(result.Data.SingleOrDefault(m =>
86-
m.Email == userEmail2 && m.Type == OrganizationUserType.Owner));
87-
Assert.NotNull(result.Data.SingleOrDefault(m =>
88-
m.Email == userEmail3 && m.Type == OrganizationUserType.User));
89-
Assert.NotNull(result.Data.SingleOrDefault(m =>
90-
m.Email == userEmail4 && m.Type == OrganizationUserType.Admin));
95+
// Verify collections
96+
Assert.NotNull(user1Result.Collections);
97+
Assert.Equal(2, user1Result.Collections.Count());
98+
var user1Collection1 = user1Result.Collections.Single(c => c.Id == collection1.Id);
99+
Assert.False(user1Collection1.ReadOnly);
100+
Assert.False(user1Collection1.HidePasswords);
101+
Assert.True(user1Collection1.Manage);
102+
var user1Collection2 = user1Result.Collections.Single(c => c.Id == collection2.Id);
103+
Assert.False(user1Collection2.ReadOnly);
104+
Assert.True(user1Collection2.HidePasswords);
105+
Assert.False(user1Collection2.Manage);
106+
107+
// The other owner
108+
var user2Result = result.Data.SingleOrDefault(m => m.Email == userEmail2 && m.Type == OrganizationUserType.Owner);
109+
Assert.NotNull(user2Result);
110+
Assert.Empty(user2Result.Collections);
111+
112+
// The user with one collection
113+
var user3Result = result.Data.SingleOrDefault(m => m.Email == userEmail3 && m.Type == OrganizationUserType.User);
114+
Assert.NotNull(user3Result);
115+
Assert.NotNull(user3Result.Collections);
116+
Assert.Single(user3Result.Collections);
117+
var user3Collection1 = user3Result.Collections.Single(c => c.Id == collection1.Id);
118+
Assert.True(user3Collection1.ReadOnly);
119+
Assert.False(user3Collection1.HidePasswords);
120+
Assert.False(user3Collection1.Manage);
121+
122+
// The admin with no collections
123+
var user4Result = result.Data.SingleOrDefault(m => m.Email == userEmail4 && m.Type == OrganizationUserType.Admin);
124+
Assert.NotNull(user4Result);
125+
Assert.Empty(user4Result.Collections);
91126
}
92127

93128
[Fact]

test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,28 @@ public static async Task<Group> CreateGroup(ApiApplicationFactory factory, Guid
151151
return group;
152152
}
153153

154+
/// <summary>
155+
/// Creates a collection with optional user and group associations.
156+
/// </summary>
157+
public static async Task<Collection> CreateCollectionAsync(
158+
ApiApplicationFactory factory,
159+
Guid organizationId,
160+
string name,
161+
IEnumerable<CollectionAccessSelection>? users = null,
162+
IEnumerable<CollectionAccessSelection>? groups = null)
163+
{
164+
var collectionRepository = factory.GetService<ICollectionRepository>();
165+
var collection = new Collection
166+
{
167+
OrganizationId = organizationId,
168+
Name = name,
169+
Type = CollectionType.SharedCollection
170+
};
171+
172+
await collectionRepository.CreateAsync(collection, groups, users);
173+
return collection;
174+
}
175+
154176
/// <summary>
155177
/// Enables the Organization Data Ownership policy for the specified organization.
156178
/// </summary>

0 commit comments

Comments
 (0)