1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00

[EC-387] Don't count revoked users towards occupied seat count (#2256)

Also autoscale seats when restoring user if required
This commit is contained in:
Thomas Rittson 2022-09-23 14:30:39 +10:00 committed by GitHub
parent c494d344d2
commit 7c3637c8ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 49 additions and 21 deletions

View File

@ -483,9 +483,9 @@ public class AccountController : Controller
// Before any user creation - if Org User doesn't exist at this point - make sure there are enough seats to add one
if (orgUser == null && organization.Seats.HasValue)
{
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(orgId);
var occupiedSeats = await _organizationService.GetOccupiedSeatCount(organization);
var initialSeatCount = organization.Seats.Value;
var availableSeats = initialSeatCount - userCount;
var availableSeats = initialSeatCount - occupiedSeats;
var prorationDate = DateTime.UtcNow;
if (availableSeats < 1)
{

View File

@ -18,7 +18,7 @@ public class OrganizationViewModel
UserInvitedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Invited);
UserAcceptedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Accepted);
UserConfirmedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Confirmed);
UserCount = orgUsers.Count();
OccupiedSeatCount = orgUsers.Count(u => u.OccupiesOrganizationSeat);
CipherCount = ciphers.Count();
CollectionCount = collections.Count();
GroupCount = groups?.Count() ?? 0;
@ -40,7 +40,7 @@ public class OrganizationViewModel
public int UserInvitedCount { get; set; }
public int UserConfirmedCount { get; set; }
public int UserAcceptedCount { get; set; }
public int UserCount { get; set; }
public int OccupiedSeatCount { get; set; }
public int CipherCount { get; set; }
public int CollectionCount { get; set; }
public int GroupCount { get; set; }

View File

@ -11,7 +11,7 @@
<dt class="col-sm-4 col-lg-3">Users</dt>
<dd class="col-sm-8 col-lg-9">
@Model.UserCount / @(Model.Organization.Seats?.ToString() ?? "-")
@Model.OccupiedSeatCount / @(Model.Organization.Seats?.ToString() ?? "-")
(<span title="Invited">@Model.UserInvitedCount</span> /
<span title="Accepted">@Model.UserAcceptedCount</span> /
<span title="Confirmed">@Model.UserConfirmedCount</span>)

View File

@ -56,4 +56,12 @@ public class OrganizationUserUserDetails : IExternal, ITwoFactorProvidersUser
{
return Premium.GetValueOrDefault(false);
}
public bool OccupiesOrganizationSeat
{
get
{
return Status != OrganizationUserStatusType.Revoked;
}
}
}

View File

@ -64,4 +64,5 @@ public interface IOrganizationService
Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId, IUserService userService);
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService);
Task<int> GetOccupiedSeatCount(Organization organization);
}

View File

@ -44,7 +44,6 @@ public class OrganizationService : IOrganizationService
private readonly ICurrentContext _currentContext;
private readonly ILogger<OrganizationService> _logger;
public OrganizationService(
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
@ -199,10 +198,10 @@ public class OrganizationService : IOrganizationService
(newPlan.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));
if (!organization.Seats.HasValue || organization.Seats.Value > newPlanSeats)
{
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
if (userCount > newPlanSeats)
var occupiedSeats = await GetOccupiedSeatCount(organization);
if (occupiedSeats > newPlanSeats)
{
throw new BadRequestException($"Your organization currently has {userCount} seats filled. " +
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
$"Your new plan only has ({newPlanSeats}) seats. Remove some users.");
}
}
@ -494,10 +493,10 @@ public class OrganizationService : IOrganizationService
if (!organization.Seats.HasValue || organization.Seats.Value > newSeatTotal)
{
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
if (userCount > newSeatTotal)
var occupiedSeats = await GetOccupiedSeatCount(organization);
if (occupiedSeats > newSeatTotal)
{
throw new BadRequestException($"Your organization currently has {userCount} seats filled. " +
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
$"Your new plan only has ({newSeatTotal}) seats. Remove some users.");
}
}
@ -861,10 +860,10 @@ public class OrganizationService : IOrganizationService
if (license.Seats.HasValue &&
(!organization.Seats.HasValue || organization.Seats.Value > license.Seats.Value))
{
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
if (userCount > license.Seats.Value)
var occupiedSeats = await GetOccupiedSeatCount(organization);
if (occupiedSeats > license.Seats.Value)
{
throw new BadRequestException($"Your organization currently has {userCount} seats filled. " +
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
$"Your new license only has ({license.Seats.Value}) seats. Remove some users.");
}
}
@ -1138,8 +1137,8 @@ public class OrganizationService : IOrganizationService
organizationId, invites.SelectMany(i => i.invite.Emails), false), StringComparer.InvariantCultureIgnoreCase);
if (organization.Seats.HasValue)
{
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organizationId);
var availableSeats = organization.Seats.Value - userCount;
var occupiedSeats = await GetOccupiedSeatCount(organization);
var availableSeats = organization.Seats.Value - occupiedSeats;
newSeatsRequired = invites.Sum(i => i.invite.Emails.Count()) - existingEmails.Count() - availableSeats;
}
@ -1559,7 +1558,7 @@ public class OrganizationService : IOrganizationService
organization.MaxAutoscaleSeats.HasValue &&
organization.MaxAutoscaleSeats.Value < organization.Seats.Value + seatsToAdd)
{
return (false, $"Cannot invite new users. Seat limit has been reached.");
return (false, $"Seat limit has been reached.");
}
return (true, failureReason);
@ -1951,8 +1950,8 @@ public class OrganizationService : IOrganizationService
var enoughSeatsAvailable = true;
if (organization.Seats.HasValue)
{
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organizationId);
seatsAvailable = organization.Seats.Value - userCount;
var occupiedSeats = await GetOccupiedSeatCount(organization);
seatsAvailable = organization.Seats.Value - occupiedSeats;
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
}
@ -2324,6 +2323,14 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException("Only owners can restore other owners.");
}
var organization = await _organizationRepository.GetByIdAsync(organizationUser.OrganizationId);
var occupiedSeats = await GetOccupiedSeatCount(organization);
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
if (availableSeats < 1)
{
await AutoAddSeatsAsync(organization, 1, DateTime.UtcNow);
}
await CheckPoliciesBeforeRestoreAsync(organizationUser, userService);
var status = GetPriorActiveOrganizationUserStatusType(organizationUser);
@ -2345,6 +2352,12 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException("Users invalid.");
}
var organization = await _organizationRepository.GetByIdAsync(organizationId);
var occupiedSeats = await GetOccupiedSeatCount(organization);
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
var newSeatsRequired = organizationUserIds.Count() - availableSeats;
await AutoAddSeatsAsync(organization, newSeatsRequired, DateTime.UtcNow);
var deletingUserIsOwner = false;
if (restoringUserId.HasValue)
{
@ -2455,4 +2468,10 @@ public class OrganizationService : IOrganizationService
return status;
}
public async Task<int> GetOccupiedSeatCount(Organization organization)
{
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organization.Id);
return orgUsers.Count(ou => ou.OccupiesOrganizationSeat);
}
}

View File

@ -897,7 +897,7 @@ public class OrganizationServiceTests
[BitAutoData(0, 100, 100, true, "")]
[BitAutoData(0, null, 100, true, "")]
[BitAutoData(1, 100, null, true, "")]
[BitAutoData(1, 100, 100, false, "Cannot invite new users. Seat limit has been reached")]
[BitAutoData(1, 100, 100, false, "Seat limit has been reached")]
public void CanScale(int seatsToAdd, int? currentSeats, int? maxAutoscaleSeats,
bool expectedResult, string expectedFailureMessage, Organization organization,
SutProvider<OrganizationService> sutProvider)