diff --git a/src/Api/AdminConsole/Public/Controllers/GroupsController.cs b/src/Api/AdminConsole/Public/Controllers/GroupsController.cs index 4113014ac3..6ced361771 100644 --- a/src/Api/AdminConsole/Public/Controllers/GroupsController.cs +++ b/src/Api/AdminConsole/Public/Controllers/GroupsController.cs @@ -110,8 +110,8 @@ public class GroupsController : Controller public async Task Post([FromBody] GroupCreateUpdateRequestModel model) { var group = model.ToGroup(_currentContext.OrganizationId.Value); - var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection()); var organization = await _organizationRepository.GetByIdAsync(_currentContext.OrganizationId.Value); + var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection(organization.FlexibleCollections)); await _createGroupCommand.CreateGroupAsync(group, organization, associations); var response = new GroupResponseModel(group, associations); return new JsonResult(response); @@ -139,8 +139,8 @@ public class GroupsController : Controller } var updatedGroup = model.ToGroup(existingGroup); - var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection()); var organization = await _organizationRepository.GetByIdAsync(_currentContext.OrganizationId.Value); + var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection(organization.FlexibleCollections)); await _updateGroupCommand.UpdateGroupAsync(updatedGroup, organization, associations); var response = new GroupResponseModel(updatedGroup, associations); return new JsonResult(response); diff --git a/src/Api/AdminConsole/Public/Controllers/MembersController.cs b/src/Api/AdminConsole/Public/Controllers/MembersController.cs index 3fde19faa7..754c3130d0 100644 --- a/src/Api/AdminConsole/Public/Controllers/MembersController.cs +++ b/src/Api/AdminConsole/Public/Controllers/MembersController.cs @@ -23,6 +23,7 @@ public class MembersController : Controller private readonly IUserService _userService; private readonly ICurrentContext _currentContext; private readonly IUpdateOrganizationUserGroupsCommand _updateOrganizationUserGroupsCommand; + private readonly IApplicationCacheService _applicationCacheService; public MembersController( IOrganizationUserRepository organizationUserRepository, @@ -30,7 +31,8 @@ public class MembersController : Controller IOrganizationService organizationService, IUserService userService, ICurrentContext currentContext, - IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand) + IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand, + IApplicationCacheService applicationCacheService) { _organizationUserRepository = organizationUserRepository; _groupRepository = groupRepository; @@ -38,6 +40,7 @@ public class MembersController : Controller _userService = userService; _currentContext = currentContext; _updateOrganizationUserGroupsCommand = updateOrganizationUserGroupsCommand; + _applicationCacheService = applicationCacheService; } /// @@ -119,7 +122,8 @@ public class MembersController : Controller [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)] public async Task Post([FromBody] MemberCreateRequestModel model) { - var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection()); + var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(_currentContext.OrganizationId.Value); + var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection(organizationAbility?.FlexibleCollections ?? false)); var invite = new OrganizationUserInvite { Emails = new List { model.Email }, @@ -154,7 +158,8 @@ public class MembersController : Controller return new NotFoundResult(); } var updatedUser = model.ToOrganizationUser(existingUser); - var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection()); + var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(_currentContext.OrganizationId.Value); + var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection(organizationAbility?.FlexibleCollections ?? false)); await _organizationService.SaveUserAsync(updatedUser, null, associations, model.Groups); MemberResponseModel response = null; if (existingUser.UserId.HasValue) diff --git a/src/Api/AdminConsole/Public/Models/AssociationWithPermissionsBaseModel.cs b/src/Api/AdminConsole/Public/Models/AssociationWithPermissionsBaseModel.cs index 4e24d2462f..72bbe87b92 100644 --- a/src/Api/AdminConsole/Public/Models/AssociationWithPermissionsBaseModel.cs +++ b/src/Api/AdminConsole/Public/Models/AssociationWithPermissionsBaseModel.cs @@ -20,4 +20,9 @@ public abstract class AssociationWithPermissionsBaseModel /// This prevents easy copy-and-paste of hidden items, however it may not completely prevent user access. /// public bool? HidePasswords { get; set; } + /// + /// 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. + /// This field will not affect behavior until the Flexible Collections functionality is released in Q1, 2024. + /// + public bool? Manage { get; set; } } diff --git a/src/Api/AdminConsole/Public/Models/Request/AssociationWithPermissionsRequestModel.cs b/src/Api/AdminConsole/Public/Models/Request/AssociationWithPermissionsRequestModel.cs index fcf5a68ba2..b54fe60b27 100644 --- a/src/Api/AdminConsole/Public/Models/Request/AssociationWithPermissionsRequestModel.cs +++ b/src/Api/AdminConsole/Public/Models/Request/AssociationWithPermissionsRequestModel.cs @@ -1,16 +1,27 @@ -using Bit.Core.Models.Data; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; namespace Bit.Api.AdminConsole.Public.Models.Request; public class AssociationWithPermissionsRequestModel : AssociationWithPermissionsBaseModel { - public CollectionAccessSelection ToCollectionAccessSelection() + public CollectionAccessSelection ToCollectionAccessSelection(bool migratedToFlexibleCollections) { - return new CollectionAccessSelection + var collectionAccessSelection = new CollectionAccessSelection { Id = Id.Value, ReadOnly = ReadOnly.Value, - HidePasswords = HidePasswords.GetValueOrDefault() + HidePasswords = HidePasswords.GetValueOrDefault(), + Manage = Manage.GetValueOrDefault() }; + + // Throws if the org has not migrated to use FC but has passed in a Manage value in the request + if (!migratedToFlexibleCollections && Manage.HasValue) + { + throw new BadRequestException( + "Your organization must be using the latest collection enhancements to use the Manage property."); + } + + return collectionAccessSelection; } } diff --git a/src/Api/AdminConsole/Public/Models/Response/AssociationWithPermissionsResponseModel.cs b/src/Api/AdminConsole/Public/Models/Response/AssociationWithPermissionsResponseModel.cs index 798234e7a6..e319ead8a4 100644 --- a/src/Api/AdminConsole/Public/Models/Response/AssociationWithPermissionsResponseModel.cs +++ b/src/Api/AdminConsole/Public/Models/Response/AssociationWithPermissionsResponseModel.cs @@ -13,5 +13,6 @@ public class AssociationWithPermissionsResponseModel : AssociationWithPermission Id = selection.Id; ReadOnly = selection.ReadOnly; HidePasswords = selection.HidePasswords; + Manage = selection.Manage; } } diff --git a/src/Api/Public/Controllers/CollectionsController.cs b/src/Api/Public/Controllers/CollectionsController.cs index 97f082cb8a..ecd84aa728 100644 --- a/src/Api/Public/Controllers/CollectionsController.cs +++ b/src/Api/Public/Controllers/CollectionsController.cs @@ -16,15 +16,18 @@ public class CollectionsController : Controller private readonly ICollectionRepository _collectionRepository; private readonly ICollectionService _collectionService; private readonly ICurrentContext _currentContext; + private readonly IApplicationCacheService _applicationCacheService; public CollectionsController( ICollectionRepository collectionRepository, ICollectionService collectionService, - ICurrentContext currentContext) + ICurrentContext currentContext, + IApplicationCacheService applicationCacheService) { _collectionRepository = collectionRepository; _collectionService = collectionService; _currentContext = currentContext; + _applicationCacheService = applicationCacheService; } /// @@ -89,7 +92,8 @@ public class CollectionsController : Controller return new NotFoundResult(); } var updatedCollection = model.ToCollection(existingCollection); - var associations = model.Groups?.Select(c => c.ToCollectionAccessSelection()); + var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(_currentContext.OrganizationId.Value); + var associations = model.Groups?.Select(c => c.ToCollectionAccessSelection(organizationAbility?.FlexibleCollections ?? false)); await _collectionService.SaveAsync(updatedCollection, associations); var response = new CollectionResponseModel(updatedCollection, associations); return new JsonResult(response);