mirror of
https://github.com/bitwarden/server.git
synced 2025-04-06 13:38:13 -05:00
[AC-2114] Downgrade Custom roles to User if flexible collections are enabled and only active permissions are 'Edit/Delete assigned collections' (#3770)
* [AC-2114] Downgrade Custom roles to User if flexible collections are enabled and only active permissions are 'Edit/Delete assigned collections' * [AC-2114] Undo changes to OrganizationsController * [AC-2114] Updated public API MembersController responses to have downgraded Custom user types for flexible collections
This commit is contained in:
parent
58b54692b2
commit
a9b9231cfa
@ -12,6 +12,7 @@ using Bit.Core.Context;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
@ -83,6 +84,15 @@ public class OrganizationUsersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var response = new OrganizationUserDetailsResponseModel(organizationUser.Item1, organizationUser.Item2);
|
var response = new OrganizationUserDetailsResponseModel(organizationUser.Item1, organizationUser.Item2);
|
||||||
|
if (await FlexibleCollectionsIsEnabledAsync(organizationUser.Item1.OrganizationId))
|
||||||
|
{
|
||||||
|
// Downgrade Custom users with no other permissions than 'Edit/Delete Assigned Collections' to User
|
||||||
|
response.Type = GetFlexibleCollectionsUserType(response.Type, response.Permissions);
|
||||||
|
|
||||||
|
// Set 'Edit/Delete Assigned Collections' custom permissions to false
|
||||||
|
response.Permissions.EditAssignedCollections = false;
|
||||||
|
response.Permissions.DeleteAssignedCollections = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (includeGroups)
|
if (includeGroups)
|
||||||
{
|
{
|
||||||
@ -95,9 +105,12 @@ public class OrganizationUsersController : Controller
|
|||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
public async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false)
|
public async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false)
|
||||||
{
|
{
|
||||||
var authorized = await FlexibleCollectionsIsEnabledAsync(orgId)
|
if (await FlexibleCollectionsIsEnabledAsync(orgId))
|
||||||
? (await _authorizationService.AuthorizeAsync(User, OrganizationUserOperations.ReadAll(orgId))).Succeeded
|
{
|
||||||
: await _currentContext.ViewAllCollections(orgId) ||
|
return await Get_vNext(orgId, includeGroups, includeCollections);
|
||||||
|
}
|
||||||
|
|
||||||
|
var authorized = await _currentContext.ViewAllCollections(orgId) ||
|
||||||
await _currentContext.ViewAssignedCollections(orgId) ||
|
await _currentContext.ViewAssignedCollections(orgId) ||
|
||||||
await _currentContext.ManageGroups(orgId) ||
|
await _currentContext.ManageGroups(orgId) ||
|
||||||
await _currentContext.ManageUsers(orgId);
|
await _currentContext.ManageUsers(orgId);
|
||||||
@ -521,4 +534,65 @@ public class OrganizationUsersController : Controller
|
|||||||
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
|
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
|
||||||
return organizationAbility?.FlexibleCollections ?? false;
|
return organizationAbility?.FlexibleCollections ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get_vNext(Guid orgId,
|
||||||
|
bool includeGroups = false, bool includeCollections = false)
|
||||||
|
{
|
||||||
|
var authorized = (await _authorizationService.AuthorizeAsync(
|
||||||
|
User, OrganizationUserOperations.ReadAll(orgId))).Succeeded;
|
||||||
|
if (!authorized)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var organizationUsers = await _organizationUserRepository
|
||||||
|
.GetManyDetailsByOrganizationAsync(orgId, includeGroups, includeCollections);
|
||||||
|
var responseTasks = organizationUsers
|
||||||
|
.Select(async o =>
|
||||||
|
{
|
||||||
|
var orgUser = new OrganizationUserUserDetailsResponseModel(o,
|
||||||
|
await _userService.TwoFactorIsEnabledAsync(o));
|
||||||
|
|
||||||
|
// Downgrade Custom users with no other permissions than 'Edit/Delete Assigned Collections' to User
|
||||||
|
orgUser.Type = GetFlexibleCollectionsUserType(orgUser.Type, orgUser.Permissions);
|
||||||
|
|
||||||
|
// Set 'Edit/Delete Assigned Collections' custom permissions to false
|
||||||
|
orgUser.Permissions.EditAssignedCollections = false;
|
||||||
|
orgUser.Permissions.DeleteAssignedCollections = false;
|
||||||
|
|
||||||
|
return orgUser;
|
||||||
|
});
|
||||||
|
var responses = await Task.WhenAll(responseTasks);
|
||||||
|
|
||||||
|
return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(responses);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OrganizationUserType GetFlexibleCollectionsUserType(OrganizationUserType type, Permissions permissions)
|
||||||
|
{
|
||||||
|
// Downgrade Custom users with no other permissions than 'Edit/Delete Assigned Collections' to User
|
||||||
|
if (type == OrganizationUserType.Custom)
|
||||||
|
{
|
||||||
|
if ((permissions.EditAssignedCollections || permissions.DeleteAssignedCollections) &&
|
||||||
|
permissions is
|
||||||
|
{
|
||||||
|
AccessEventLogs: false,
|
||||||
|
AccessImportExport: false,
|
||||||
|
AccessReports: false,
|
||||||
|
CreateNewCollections: false,
|
||||||
|
EditAnyCollection: false,
|
||||||
|
DeleteAnyCollection: false,
|
||||||
|
ManageGroups: false,
|
||||||
|
ManagePolicies: false,
|
||||||
|
ManageSso: false,
|
||||||
|
ManageUsers: false,
|
||||||
|
ManageResetPassword: false,
|
||||||
|
ManageScim: false
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return OrganizationUserType.User;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return type;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,6 +69,37 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
KeyConnectorEnabled = ssoConfigData.MemberDecryptionType == MemberDecryptionType.KeyConnector && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl);
|
KeyConnectorEnabled = ssoConfigData.MemberDecryptionType == MemberDecryptionType.KeyConnector && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl);
|
||||||
KeyConnectorUrl = ssoConfigData.KeyConnectorUrl;
|
KeyConnectorUrl = ssoConfigData.KeyConnectorUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (FlexibleCollections)
|
||||||
|
{
|
||||||
|
// Downgrade Custom users with no other permissions than 'Edit/Delete Assigned Collections' to User
|
||||||
|
if (Type == OrganizationUserType.Custom)
|
||||||
|
{
|
||||||
|
if ((Permissions.EditAssignedCollections || Permissions.DeleteAssignedCollections) &&
|
||||||
|
Permissions is
|
||||||
|
{
|
||||||
|
AccessEventLogs: false,
|
||||||
|
AccessImportExport: false,
|
||||||
|
AccessReports: false,
|
||||||
|
CreateNewCollections: false,
|
||||||
|
EditAnyCollection: false,
|
||||||
|
DeleteAnyCollection: false,
|
||||||
|
ManageGroups: false,
|
||||||
|
ManagePolicies: false,
|
||||||
|
ManageSso: false,
|
||||||
|
ManageUsers: false,
|
||||||
|
ManageResetPassword: false,
|
||||||
|
ManageScim: false
|
||||||
|
})
|
||||||
|
{
|
||||||
|
organization.Type = OrganizationUserType.User;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set 'Edit/Delete Assigned Collections' custom permissions to false
|
||||||
|
Permissions.EditAssignedCollections = false;
|
||||||
|
Permissions.DeleteAssignedCollections = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
@ -61,8 +61,9 @@ public class MembersController : Controller
|
|||||||
{
|
{
|
||||||
return new NotFoundResult();
|
return new NotFoundResult();
|
||||||
}
|
}
|
||||||
|
var flexibleCollectionsIsEnabled = await FlexibleCollectionsIsEnabledAsync(orgUser.OrganizationId);
|
||||||
var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser),
|
var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser),
|
||||||
userDetails.Item2);
|
userDetails.Item2, flexibleCollectionsIsEnabled);
|
||||||
return new JsonResult(response);
|
return new JsonResult(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,9 +102,10 @@ public class MembersController : Controller
|
|||||||
{
|
{
|
||||||
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(
|
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(
|
||||||
_currentContext.OrganizationId.Value);
|
_currentContext.OrganizationId.Value);
|
||||||
|
var flexibleCollectionsIsEnabled = await FlexibleCollectionsIsEnabledAsync(_currentContext.OrganizationId.Value);
|
||||||
// TODO: Get all CollectionUser associations for the organization and marry them up here for the response.
|
// TODO: Get all CollectionUser associations for the organization and marry them up here for the response.
|
||||||
var memberResponsesTasks = users.Select(async u => new MemberResponseModel(u,
|
var memberResponsesTasks = users.Select(async u => new MemberResponseModel(u,
|
||||||
await _userService.TwoFactorIsEnabledAsync(u), null));
|
await _userService.TwoFactorIsEnabledAsync(u), null, flexibleCollectionsIsEnabled));
|
||||||
var memberResponses = await Task.WhenAll(memberResponsesTasks);
|
var memberResponses = await Task.WhenAll(memberResponsesTasks);
|
||||||
var response = new ListResponseModel<MemberResponseModel>(memberResponses);
|
var response = new ListResponseModel<MemberResponseModel>(memberResponses);
|
||||||
return new JsonResult(response);
|
return new JsonResult(response);
|
||||||
@ -121,11 +123,11 @@ public class MembersController : Controller
|
|||||||
[ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
|
[ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
|
||||||
public async Task<IActionResult> Post([FromBody] MemberCreateRequestModel model)
|
public async Task<IActionResult> Post([FromBody] MemberCreateRequestModel model)
|
||||||
{
|
{
|
||||||
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(_currentContext.OrganizationId.Value);
|
var flexibleCollectionsIsEnabled = await FlexibleCollectionsIsEnabledAsync(_currentContext.OrganizationId.Value);
|
||||||
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection(organizationAbility?.FlexibleCollections ?? false)).ToList();
|
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection(flexibleCollectionsIsEnabled)).ToList();
|
||||||
var user = await _organizationService.InviteUserAsync(_currentContext.OrganizationId.Value, null,
|
var user = await _organizationService.InviteUserAsync(_currentContext.OrganizationId.Value, null,
|
||||||
model.Email, model.Type.Value, model.AccessAll.Value, model.ExternalId, associations, model.Groups);
|
model.Email, model.Type.Value, model.AccessAll.Value, model.ExternalId, associations, model.Groups);
|
||||||
var response = new MemberResponseModel(user, associations);
|
var response = new MemberResponseModel(user, associations, flexibleCollectionsIsEnabled);
|
||||||
return new JsonResult(response);
|
return new JsonResult(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,19 +152,19 @@ public class MembersController : Controller
|
|||||||
return new NotFoundResult();
|
return new NotFoundResult();
|
||||||
}
|
}
|
||||||
var updatedUser = model.ToOrganizationUser(existingUser);
|
var updatedUser = model.ToOrganizationUser(existingUser);
|
||||||
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(_currentContext.OrganizationId.Value);
|
var flexibleCollectionsIsEnabled = await FlexibleCollectionsIsEnabledAsync(_currentContext.OrganizationId.Value);
|
||||||
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection(organizationAbility?.FlexibleCollections ?? false)).ToList();
|
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection(flexibleCollectionsIsEnabled)).ToList();
|
||||||
await _organizationService.SaveUserAsync(updatedUser, null, associations, model.Groups);
|
await _organizationService.SaveUserAsync(updatedUser, null, associations, model.Groups);
|
||||||
MemberResponseModel response = null;
|
MemberResponseModel response = null;
|
||||||
if (existingUser.UserId.HasValue)
|
if (existingUser.UserId.HasValue)
|
||||||
{
|
{
|
||||||
var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
||||||
response = new MemberResponseModel(existingUserDetails,
|
response = new MemberResponseModel(existingUserDetails,
|
||||||
await _userService.TwoFactorIsEnabledAsync(existingUserDetails), associations);
|
await _userService.TwoFactorIsEnabledAsync(existingUserDetails), associations, flexibleCollectionsIsEnabled);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
response = new MemberResponseModel(updatedUser, associations);
|
response = new MemberResponseModel(updatedUser, associations, flexibleCollectionsIsEnabled);
|
||||||
}
|
}
|
||||||
return new JsonResult(response);
|
return new JsonResult(response);
|
||||||
}
|
}
|
||||||
@ -233,4 +235,10 @@ public class MembersController : Controller
|
|||||||
await _organizationService.ResendInviteAsync(_currentContext.OrganizationId.Value, null, id);
|
await _organizationService.ResendInviteAsync(_currentContext.OrganizationId.Value, null, id);
|
||||||
return new OkResult();
|
return new OkResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> FlexibleCollectionsIsEnabledAsync(Guid organizationId)
|
||||||
|
{
|
||||||
|
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
|
||||||
|
return organizationAbility?.FlexibleCollections ?? false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
|
||||||
namespace Bit.Api.AdminConsole.Public.Models;
|
namespace Bit.Api.AdminConsole.Public.Models;
|
||||||
@ -9,27 +10,27 @@ public abstract class MemberBaseModel
|
|||||||
{
|
{
|
||||||
public MemberBaseModel() { }
|
public MemberBaseModel() { }
|
||||||
|
|
||||||
public MemberBaseModel(OrganizationUser user)
|
public MemberBaseModel(OrganizationUser user, bool flexibleCollectionsEnabled)
|
||||||
{
|
{
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(user));
|
throw new ArgumentNullException(nameof(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
Type = user.Type;
|
Type = flexibleCollectionsEnabled ? GetFlexibleCollectionsUserType(user.Type, user.GetPermissions()) : user.Type;
|
||||||
AccessAll = user.AccessAll;
|
AccessAll = user.AccessAll;
|
||||||
ExternalId = user.ExternalId;
|
ExternalId = user.ExternalId;
|
||||||
ResetPasswordEnrolled = user.ResetPasswordKey != null;
|
ResetPasswordEnrolled = user.ResetPasswordKey != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MemberBaseModel(OrganizationUserUserDetails user)
|
public MemberBaseModel(OrganizationUserUserDetails user, bool flexibleCollectionsEnabled)
|
||||||
{
|
{
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(user));
|
throw new ArgumentNullException(nameof(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
Type = user.Type;
|
Type = flexibleCollectionsEnabled ? GetFlexibleCollectionsUserType(user.Type, user.GetPermissions()) : user.Type;
|
||||||
AccessAll = user.AccessAll;
|
AccessAll = user.AccessAll;
|
||||||
ExternalId = user.ExternalId;
|
ExternalId = user.ExternalId;
|
||||||
ResetPasswordEnrolled = user.ResetPasswordKey != null;
|
ResetPasswordEnrolled = user.ResetPasswordKey != null;
|
||||||
@ -58,4 +59,34 @@ public abstract class MemberBaseModel
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
public bool ResetPasswordEnrolled { get; set; }
|
public bool ResetPasswordEnrolled { get; set; }
|
||||||
|
|
||||||
|
// TODO: AC-2188 - Remove this method when the custom users with no other permissions than 'Edit/Delete Assigned Collections' are migrated
|
||||||
|
private OrganizationUserType GetFlexibleCollectionsUserType(OrganizationUserType type, Permissions permissions)
|
||||||
|
{
|
||||||
|
// Downgrade Custom users with no other permissions than 'Edit/Delete Assigned Collections' to User
|
||||||
|
if (type == OrganizationUserType.Custom)
|
||||||
|
{
|
||||||
|
if ((permissions.EditAssignedCollections || permissions.DeleteAssignedCollections) &&
|
||||||
|
permissions is
|
||||||
|
{
|
||||||
|
AccessEventLogs: false,
|
||||||
|
AccessImportExport: false,
|
||||||
|
AccessReports: false,
|
||||||
|
CreateNewCollections: false,
|
||||||
|
EditAnyCollection: false,
|
||||||
|
DeleteAnyCollection: false,
|
||||||
|
ManageGroups: false,
|
||||||
|
ManagePolicies: false,
|
||||||
|
ManageSso: false,
|
||||||
|
ManageUsers: false,
|
||||||
|
ManageResetPassword: false,
|
||||||
|
ManageScim: false
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return OrganizationUserType.User;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return type;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,9 @@ namespace Bit.Api.AdminConsole.Public.Models.Response;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class MemberResponseModel : MemberBaseModel, IResponseModel
|
public class MemberResponseModel : MemberBaseModel, IResponseModel
|
||||||
{
|
{
|
||||||
public MemberResponseModel(OrganizationUser user, IEnumerable<CollectionAccessSelection> collections)
|
public MemberResponseModel(OrganizationUser user, IEnumerable<CollectionAccessSelection> collections,
|
||||||
: base(user)
|
bool flexibleCollectionsEnabled)
|
||||||
|
: base(user, flexibleCollectionsEnabled)
|
||||||
{
|
{
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
@ -28,8 +29,8 @@ public class MemberResponseModel : MemberBaseModel, IResponseModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
public MemberResponseModel(OrganizationUserUserDetails user, bool twoFactorEnabled,
|
public MemberResponseModel(OrganizationUserUserDetails user, bool twoFactorEnabled,
|
||||||
IEnumerable<CollectionAccessSelection> collections)
|
IEnumerable<CollectionAccessSelection> collections, bool flexibleCollectionsEnabled)
|
||||||
: base(user)
|
: base(user, flexibleCollectionsEnabled)
|
||||||
{
|
{
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user