Skip to content

Commit 6524c55

Browse files
authored
Merge branch 'main' into workflow-lint
2 parents 6b41c28 + aeca172 commit 6524c55

File tree

17 files changed

+327
-72
lines changed

17 files changed

+327
-72
lines changed

bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,10 @@ await _organizationService.InviteUsersAsync(organization.Id, user.Id,
515515
new OrganizationUserInvite
516516
{
517517
Emails = new[] { clientOwnerEmail },
518-
AccessAll = true,
518+
519+
// If using Flexible Collections, AccessAll is deprecated and set to false.
520+
// If not using Flexible Collections, set AccessAll to true (previous behavior)
521+
AccessAll = !organization.FlexibleCollections,
519522
Type = OrganizationUserType.Owner,
520523
Permissions = null,
521524
Collections = Array.Empty<CollectionAccessSelection>(),

bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs

+31-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Bit.Core.Models.Business;
1313
using Bit.Core.Repositories;
1414
using Bit.Core.Services;
15+
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
1516
using Bit.Core.Utilities;
1617
using Bit.Test.Common.AutoFixture;
1718
using Bit.Test.Common.AutoFixture.Attributes;
@@ -513,7 +514,7 @@ public async Task AddOrganizationsToReseller_WithMspProvider_Throws(Provider pro
513514
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogProviderOrganizationEventsAsync(default);
514515
}
515516

516-
[Theory, BitAutoData]
517+
[Theory, OrganizationCustomize(FlexibleCollections = false), BitAutoData]
517518
public async Task CreateOrganizationAsync_Success(Provider provider, OrganizationSignup organizationSignup,
518519
Organization organization, string clientOwnerEmail, User user, SutProvider<ProviderService> sutProvider)
519520
{
@@ -541,6 +542,35 @@ await sutProvider.GetDependency<IOrganizationService>()
541542
t.First().Item2 == null));
542543
}
543544

545+
[Theory, OrganizationCustomize(FlexibleCollections = true), BitAutoData]
546+
public async Task CreateOrganizationAsync_WithFlexibleCollections_SetsAccessAllToFalse
547+
(Provider provider, OrganizationSignup organizationSignup, Organization organization, string clientOwnerEmail,
548+
User user, SutProvider<ProviderService> sutProvider)
549+
{
550+
organizationSignup.Plan = PlanType.EnterpriseAnnually;
551+
552+
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
553+
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
554+
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup, true)
555+
.Returns(Tuple.Create(organization, null as OrganizationUser));
556+
557+
var providerOrganization =
558+
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
559+
560+
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);
561+
await sutProvider.GetDependency<IEventService>()
562+
.Received().LogProviderOrganizationEventAsync(providerOrganization,
563+
EventType.ProviderOrganization_Created);
564+
await sutProvider.GetDependency<IOrganizationService>()
565+
.Received().InviteUsersAsync(organization.Id, user.Id, Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(
566+
t => t.Count() == 1 &&
567+
t.First().Item1.Emails.Count() == 1 &&
568+
t.First().Item1.Emails.First() == clientOwnerEmail &&
569+
t.First().Item1.Type == OrganizationUserType.Owner &&
570+
t.First().Item1.AccessAll == false &&
571+
t.First().Item2 == null));
572+
}
573+
544574
[Theory, BitAutoData]
545575
public async Task AddOrganization_CreateAfterNov162023_PlanTypeDoesNotUpdated(Provider provider, Organization organization, string key,
546576
SutProvider<ProviderService> sutProvider)

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ public async Task<IActionResult> List()
110110
public async Task<IActionResult> Post([FromBody] GroupCreateUpdateRequestModel model)
111111
{
112112
var group = model.ToGroup(_currentContext.OrganizationId.Value);
113-
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection());
114113
var organization = await _organizationRepository.GetByIdAsync(_currentContext.OrganizationId.Value);
114+
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection(organization.FlexibleCollections));
115115
await _createGroupCommand.CreateGroupAsync(group, organization, associations);
116116
var response = new GroupResponseModel(group, associations);
117117
return new JsonResult(response);
@@ -139,8 +139,8 @@ public async Task<IActionResult> Put(Guid id, [FromBody] GroupCreateUpdateReques
139139
}
140140

141141
var updatedGroup = model.ToGroup(existingGroup);
142-
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection());
143142
var organization = await _organizationRepository.GetByIdAsync(_currentContext.OrganizationId.Value);
143+
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection(organization.FlexibleCollections));
144144
await _updateGroupCommand.UpdateGroupAsync(updatedGroup, organization, associations);
145145
var response = new GroupResponseModel(updatedGroup, associations);
146146
return new JsonResult(response);

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

+8-3
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,24 @@ public class MembersController : Controller
2323
private readonly IUserService _userService;
2424
private readonly ICurrentContext _currentContext;
2525
private readonly IUpdateOrganizationUserGroupsCommand _updateOrganizationUserGroupsCommand;
26+
private readonly IApplicationCacheService _applicationCacheService;
2627

2728
public MembersController(
2829
IOrganizationUserRepository organizationUserRepository,
2930
IGroupRepository groupRepository,
3031
IOrganizationService organizationService,
3132
IUserService userService,
3233
ICurrentContext currentContext,
33-
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand)
34+
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand,
35+
IApplicationCacheService applicationCacheService)
3436
{
3537
_organizationUserRepository = organizationUserRepository;
3638
_groupRepository = groupRepository;
3739
_organizationService = organizationService;
3840
_userService = userService;
3941
_currentContext = currentContext;
4042
_updateOrganizationUserGroupsCommand = updateOrganizationUserGroupsCommand;
43+
_applicationCacheService = applicationCacheService;
4144
}
4245

4346
/// <summary>
@@ -119,7 +122,8 @@ public async Task<IActionResult> List()
119122
[ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
120123
public async Task<IActionResult> Post([FromBody] MemberCreateRequestModel model)
121124
{
122-
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection());
125+
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(_currentContext.OrganizationId.Value);
126+
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection(organizationAbility?.FlexibleCollections ?? false));
123127
var invite = new OrganizationUserInvite
124128
{
125129
Emails = new List<string> { model.Email },
@@ -154,7 +158,8 @@ public async Task<IActionResult> Put(Guid id, [FromBody] MemberUpdateRequestMode
154158
return new NotFoundResult();
155159
}
156160
var updatedUser = model.ToOrganizationUser(existingUser);
157-
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection());
161+
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(_currentContext.OrganizationId.Value);
162+
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection(organizationAbility?.FlexibleCollections ?? false));
158163
await _organizationService.SaveUserAsync(updatedUser, null, associations, model.Groups);
159164
MemberResponseModel response = null;
160165
if (existingUser.UserId.HasValue)

src/Api/AdminConsole/Public/Models/AssociationWithPermissionsBaseModel.cs

+5
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,9 @@ public abstract class AssociationWithPermissionsBaseModel
2020
/// This prevents easy copy-and-paste of hidden items, however it may not completely prevent user access.
2121
/// </summary>
2222
public bool? HidePasswords { get; set; }
23+
/// <summary>
24+
/// When true, the manage permission allows a user to both edit the ciphers within a collection and edit the users/groups that are assigned to the collection.
25+
/// This field will not affect behavior until the Flexible Collections functionality is released in Q1, 2024.
26+
/// </summary>
27+
public bool? Manage { get; set; }
2328
}
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
1-
using Bit.Core.Models.Data;
1+
using Bit.Core.Exceptions;
2+
using Bit.Core.Models.Data;
23

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

56
public class AssociationWithPermissionsRequestModel : AssociationWithPermissionsBaseModel
67
{
7-
public CollectionAccessSelection ToCollectionAccessSelection()
8+
public CollectionAccessSelection ToCollectionAccessSelection(bool migratedToFlexibleCollections)
89
{
9-
return new CollectionAccessSelection
10+
var collectionAccessSelection = new CollectionAccessSelection
1011
{
1112
Id = Id.Value,
1213
ReadOnly = ReadOnly.Value,
13-
HidePasswords = HidePasswords.GetValueOrDefault()
14+
HidePasswords = HidePasswords.GetValueOrDefault(),
15+
Manage = Manage.GetValueOrDefault()
1416
};
17+
18+
// Throws if the org has not migrated to use FC but has passed in a Manage value in the request
19+
if (!migratedToFlexibleCollections && Manage.HasValue)
20+
{
21+
throw new BadRequestException(
22+
"Your organization must be using the latest collection enhancements to use the Manage property.");
23+
}
24+
25+
return collectionAccessSelection;
1526
}
1627
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ public AssociationWithPermissionsResponseModel(CollectionAccessSelection selecti
1313
Id = selection.Id;
1414
ReadOnly = selection.ReadOnly;
1515
HidePasswords = selection.HidePasswords;
16+
Manage = selection.Manage;
1617
}
1718
}

src/Api/Public/Controllers/CollectionsController.cs

+6-2
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,18 @@ public class CollectionsController : Controller
1616
private readonly ICollectionRepository _collectionRepository;
1717
private readonly ICollectionService _collectionService;
1818
private readonly ICurrentContext _currentContext;
19+
private readonly IApplicationCacheService _applicationCacheService;
1920

2021
public CollectionsController(
2122
ICollectionRepository collectionRepository,
2223
ICollectionService collectionService,
23-
ICurrentContext currentContext)
24+
ICurrentContext currentContext,
25+
IApplicationCacheService applicationCacheService)
2426
{
2527
_collectionRepository = collectionRepository;
2628
_collectionService = collectionService;
2729
_currentContext = currentContext;
30+
_applicationCacheService = applicationCacheService;
2831
}
2932

3033
/// <summary>
@@ -89,7 +92,8 @@ public async Task<IActionResult> Put(Guid id, [FromBody] CollectionUpdateRequest
8992
return new NotFoundResult();
9093
}
9194
var updatedCollection = model.ToCollection(existingCollection);
92-
var associations = model.Groups?.Select(c => c.ToCollectionAccessSelection());
95+
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(_currentContext.OrganizationId.Value);
96+
var associations = model.Groups?.Select(c => c.ToCollectionAccessSelection(organizationAbility?.FlexibleCollections ?? false));
9397
await _collectionService.SaveAsync(updatedCollection, associations);
9498
var response = new CollectionResponseModel(updatedCollection, associations);
9599
return new JsonResult(response);

src/Core/AdminConsole/OrganizationFeatures/Groups/CreateGroupCommand.cs

+8-3
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public async Task CreateGroupAsync(Group group, Organization organization,
3939
IEnumerable<CollectionAccessSelection> collections = null,
4040
IEnumerable<Guid> users = null)
4141
{
42-
Validate(organization);
42+
Validate(organization, group);
4343
await GroupRepositoryCreateGroupAsync(group, organization, collections);
4444

4545
if (users != null)
@@ -54,7 +54,7 @@ public async Task CreateGroupAsync(Group group, Organization organization, Event
5454
IEnumerable<CollectionAccessSelection> collections = null,
5555
IEnumerable<Guid> users = null)
5656
{
57-
Validate(organization);
57+
Validate(organization, group);
5858
await GroupRepositoryCreateGroupAsync(group, organization, collections);
5959

6060
if (users != null)
@@ -103,7 +103,7 @@ await _eventService.LogOrganizationUserEventsAsync(users.Select(u =>
103103
}
104104
}
105105

106-
private static void Validate(Organization organization)
106+
private static void Validate(Organization organization, Group group)
107107
{
108108
if (organization == null)
109109
{
@@ -114,5 +114,10 @@ private static void Validate(Organization organization)
114114
{
115115
throw new BadRequestException("This organization cannot use groups.");
116116
}
117+
118+
if (organization.FlexibleCollections && group.AccessAll)
119+
{
120+
throw new BadRequestException("The AccessAll property has been deprecated by collection enhancements. Assign the group to collections instead.");
121+
}
117122
}
118123
}

src/Core/AdminConsole/OrganizationFeatures/Groups/UpdateGroupCommand.cs

+8-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public async Task UpdateGroupAsync(Group group, Organization organization,
2929
IEnumerable<CollectionAccessSelection> collections = null,
3030
IEnumerable<Guid> userIds = null)
3131
{
32-
Validate(organization);
32+
Validate(organization, group);
3333
await GroupRepositoryUpdateGroupAsync(group, collections);
3434

3535
if (userIds != null)
@@ -44,7 +44,7 @@ public async Task UpdateGroupAsync(Group group, Organization organization, Event
4444
IEnumerable<CollectionAccessSelection> collections = null,
4545
IEnumerable<Guid> userIds = null)
4646
{
47-
Validate(organization);
47+
Validate(organization, group);
4848
await GroupRepositoryUpdateGroupAsync(group, collections);
4949

5050
if (userIds != null)
@@ -97,7 +97,7 @@ await _eventService.LogOrganizationUserEventsAsync(users.Select(u =>
9797
}
9898
}
9999

100-
private static void Validate(Organization organization)
100+
private static void Validate(Organization organization, Group group)
101101
{
102102
if (organization == null)
103103
{
@@ -108,5 +108,10 @@ private static void Validate(Organization organization)
108108
{
109109
throw new BadRequestException("This organization cannot use groups.");
110110
}
111+
112+
if (organization.FlexibleCollections && group.AccessAll)
113+
{
114+
throw new BadRequestException("The AccessAll property has been deprecated by collection enhancements. Assign the group to collections instead.");
115+
}
111116
}
112117
}

src/Core/AdminConsole/Services/Implementations/OrganizationService.cs

+33-11
Original file line numberDiff line numberDiff line change
@@ -673,7 +673,10 @@ await _organizationApiKeyRepository.CreateAsync(new OrganizationApiKey
673673
AccessSecretsManager = organization.UseSecretsManager,
674674
Type = OrganizationUserType.Owner,
675675
Status = OrganizationUserStatusType.Confirmed,
676-
AccessAll = true,
676+
677+
// If using Flexible Collections, AccessAll is deprecated and set to false.
678+
// If not using Flexible Collections, set AccessAll to true (previous behavior)
679+
AccessAll = !organization.FlexibleCollections,
677680
CreationDate = organization.CreationDate,
678681
RevisionDate = organization.CreationDate
679682
};
@@ -885,6 +888,18 @@ public async Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId,
885888
throw new NotFoundException();
886889
}
887890

891+
// If the organization is using Flexible Collections, prevent use of any deprecated permissions
892+
if (organization.FlexibleCollections && invites.Any(i => i.invite.Type is OrganizationUserType.Manager))
893+
{
894+
throw new BadRequestException("The Manager role has been deprecated by collection enhancements. Use the collection Can Manage permission instead.");
895+
}
896+
897+
if (organization.FlexibleCollections && invites.Any(i => i.invite.AccessAll))
898+
{
899+
throw new BadRequestException("The AccessAll property has been deprecated by collection enhancements. Assign the user to collections instead.");
900+
}
901+
// End Flexible Collections
902+
888903
var existingEmails = new HashSet<string>(await _organizationUserRepository.SelectKnownEmailsAsync(
889904
organizationId, invites.SelectMany(i => i.invite.Emails), false), StringComparer.InvariantCultureIgnoreCase);
890905

@@ -1377,6 +1392,19 @@ public async Task SaveUserAsync(OrganizationUser user, Guid? savingUserId,
13771392
throw new BadRequestException("Organization must have at least one confirmed owner.");
13781393
}
13791394

1395+
// If the organization is using Flexible Collections, prevent use of any deprecated permissions
1396+
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(user.OrganizationId);
1397+
if (organizationAbility?.FlexibleCollections == true && user.Type == OrganizationUserType.Manager)
1398+
{
1399+
throw new BadRequestException("The Manager role has been deprecated by collection enhancements. Use the collection Can Manage permission instead.");
1400+
}
1401+
1402+
if (organizationAbility?.FlexibleCollections == true && user.AccessAll)
1403+
{
1404+
throw new BadRequestException("The AccessAll property has been deprecated by collection enhancements. Assign the user to collections instead.");
1405+
}
1406+
// End Flexible Collections
1407+
13801408
// Only autoscale (if required) after all validation has passed so that we know it's a valid request before
13811409
// updating Stripe
13821410
if (!originalUser.AccessSecretsManager && user.AccessSecretsManager)
@@ -2027,15 +2055,6 @@ public async Task ValidateOrganizationUserUpdatePermissions(Guid organizationId,
20272055
{
20282056
throw new BadRequestException("Custom users can only grant the same custom permissions that they have.");
20292057
}
2030-
2031-
// TODO: pass in the whole organization object when this is refactored into a command/query
2032-
// See AC-2036
2033-
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
2034-
var flexibleCollectionsEnabled = organizationAbility?.FlexibleCollections ?? false;
2035-
if (flexibleCollectionsEnabled && newType == OrganizationUserType.Manager && oldType is not OrganizationUserType.Manager)
2036-
{
2037-
throw new BadRequestException("Manager role is deprecated after Flexible Collections.");
2038-
}
20392058
}
20402059

20412060
private async Task ValidateOrganizationCustomPermissionsEnabledAsync(Guid organizationId, OrganizationUserType newType)
@@ -2451,7 +2470,10 @@ public async Task CreatePendingOrganization(Organization organization, string ow
24512470
Key = null,
24522471
Type = OrganizationUserType.Owner,
24532472
Status = OrganizationUserStatusType.Invited,
2454-
AccessAll = true
2473+
2474+
// If using Flexible Collections, AccessAll is deprecated and set to false.
2475+
// If not using Flexible Collections, set AccessAll to true (previous behavior)
2476+
AccessAll = !organization.FlexibleCollections,
24552477
};
24562478
await _organizationUserRepository.CreateAsync(ownerOrganizationUser);
24572479

src/Core/Services/Implementations/CollectionService.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public async Task SaveAsync(Collection collection, IEnumerable<CollectionAccessS
5656
var usersList = users?.ToList();
5757

5858
// If using Flexible Collections - a collection should always have someone with Can Manage permissions
59-
if (_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1))
59+
if (org.FlexibleCollections && _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1))
6060
{
6161
var groupHasManageAccess = groupsList?.Any(g => g.Manage) ?? false;
6262
var userHasManageAccess = usersList?.Any(u => u.Manage) ?? false;

0 commit comments

Comments
 (0)