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:
parent
c494d344d2
commit
7c3637c8ba
@ -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)
|
||||
{
|
||||
|
@ -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; }
|
||||
|
@ -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>)
|
||||
|
@ -56,4 +56,12 @@ public class OrganizationUserUserDetails : IExternal, ITwoFactorProvidersUser
|
||||
{
|
||||
return Premium.GetValueOrDefault(false);
|
||||
}
|
||||
|
||||
public bool OccupiesOrganizationSeat
|
||||
{
|
||||
get
|
||||
{
|
||||
return Status != OrganizationUserStatusType.Revoked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user