mirror of
https://github.com/bitwarden/server.git
synced 2025-04-05 13:08:17 -05: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
|
// 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)
|
if (orgUser == null && organization.Seats.HasValue)
|
||||||
{
|
{
|
||||||
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(orgId);
|
var occupiedSeats = await _organizationService.GetOccupiedSeatCount(organization);
|
||||||
var initialSeatCount = organization.Seats.Value;
|
var initialSeatCount = organization.Seats.Value;
|
||||||
var availableSeats = initialSeatCount - userCount;
|
var availableSeats = initialSeatCount - occupiedSeats;
|
||||||
var prorationDate = DateTime.UtcNow;
|
var prorationDate = DateTime.UtcNow;
|
||||||
if (availableSeats < 1)
|
if (availableSeats < 1)
|
||||||
{
|
{
|
||||||
|
@ -18,7 +18,7 @@ public class OrganizationViewModel
|
|||||||
UserInvitedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Invited);
|
UserInvitedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Invited);
|
||||||
UserAcceptedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Accepted);
|
UserAcceptedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Accepted);
|
||||||
UserConfirmedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Confirmed);
|
UserConfirmedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Confirmed);
|
||||||
UserCount = orgUsers.Count();
|
OccupiedSeatCount = orgUsers.Count(u => u.OccupiesOrganizationSeat);
|
||||||
CipherCount = ciphers.Count();
|
CipherCount = ciphers.Count();
|
||||||
CollectionCount = collections.Count();
|
CollectionCount = collections.Count();
|
||||||
GroupCount = groups?.Count() ?? 0;
|
GroupCount = groups?.Count() ?? 0;
|
||||||
@ -40,7 +40,7 @@ public class OrganizationViewModel
|
|||||||
public int UserInvitedCount { get; set; }
|
public int UserInvitedCount { get; set; }
|
||||||
public int UserConfirmedCount { get; set; }
|
public int UserConfirmedCount { get; set; }
|
||||||
public int UserAcceptedCount { get; set; }
|
public int UserAcceptedCount { get; set; }
|
||||||
public int UserCount { get; set; }
|
public int OccupiedSeatCount { get; set; }
|
||||||
public int CipherCount { get; set; }
|
public int CipherCount { get; set; }
|
||||||
public int CollectionCount { get; set; }
|
public int CollectionCount { get; set; }
|
||||||
public int GroupCount { get; set; }
|
public int GroupCount { get; set; }
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<dt class="col-sm-4 col-lg-3">Users</dt>
|
<dt class="col-sm-4 col-lg-3">Users</dt>
|
||||||
<dd class="col-sm-8 col-lg-9">
|
<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="Invited">@Model.UserInvitedCount</span> /
|
||||||
<span title="Accepted">@Model.UserAcceptedCount</span> /
|
<span title="Accepted">@Model.UserAcceptedCount</span> /
|
||||||
<span title="Confirmed">@Model.UserConfirmedCount</span>)
|
<span title="Confirmed">@Model.UserConfirmedCount</span>)
|
||||||
|
@ -56,4 +56,12 @@ public class OrganizationUserUserDetails : IExternal, ITwoFactorProvidersUser
|
|||||||
{
|
{
|
||||||
return Premium.GetValueOrDefault(false);
|
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 RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId, IUserService userService);
|
||||||
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
|
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
|
||||||
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService);
|
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 ICurrentContext _currentContext;
|
||||||
private readonly ILogger<OrganizationService> _logger;
|
private readonly ILogger<OrganizationService> _logger;
|
||||||
|
|
||||||
|
|
||||||
public OrganizationService(
|
public OrganizationService(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
@ -199,10 +198,10 @@ public class OrganizationService : IOrganizationService
|
|||||||
(newPlan.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));
|
(newPlan.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));
|
||||||
if (!organization.Seats.HasValue || organization.Seats.Value > newPlanSeats)
|
if (!organization.Seats.HasValue || organization.Seats.Value > newPlanSeats)
|
||||||
{
|
{
|
||||||
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
|
var occupiedSeats = await GetOccupiedSeatCount(organization);
|
||||||
if (userCount > newPlanSeats)
|
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.");
|
$"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)
|
if (!organization.Seats.HasValue || organization.Seats.Value > newSeatTotal)
|
||||||
{
|
{
|
||||||
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
|
var occupiedSeats = await GetOccupiedSeatCount(organization);
|
||||||
if (userCount > newSeatTotal)
|
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.");
|
$"Your new plan only has ({newSeatTotal}) seats. Remove some users.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -861,10 +860,10 @@ public class OrganizationService : IOrganizationService
|
|||||||
if (license.Seats.HasValue &&
|
if (license.Seats.HasValue &&
|
||||||
(!organization.Seats.HasValue || organization.Seats.Value > license.Seats.Value))
|
(!organization.Seats.HasValue || organization.Seats.Value > license.Seats.Value))
|
||||||
{
|
{
|
||||||
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
|
var occupiedSeats = await GetOccupiedSeatCount(organization);
|
||||||
if (userCount > license.Seats.Value)
|
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.");
|
$"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);
|
organizationId, invites.SelectMany(i => i.invite.Emails), false), StringComparer.InvariantCultureIgnoreCase);
|
||||||
if (organization.Seats.HasValue)
|
if (organization.Seats.HasValue)
|
||||||
{
|
{
|
||||||
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organizationId);
|
var occupiedSeats = await GetOccupiedSeatCount(organization);
|
||||||
var availableSeats = organization.Seats.Value - userCount;
|
var availableSeats = organization.Seats.Value - occupiedSeats;
|
||||||
newSeatsRequired = invites.Sum(i => i.invite.Emails.Count()) - existingEmails.Count() - availableSeats;
|
newSeatsRequired = invites.Sum(i => i.invite.Emails.Count()) - existingEmails.Count() - availableSeats;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1559,7 +1558,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
organization.MaxAutoscaleSeats.HasValue &&
|
organization.MaxAutoscaleSeats.HasValue &&
|
||||||
organization.MaxAutoscaleSeats.Value < organization.Seats.Value + seatsToAdd)
|
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);
|
return (true, failureReason);
|
||||||
@ -1951,8 +1950,8 @@ public class OrganizationService : IOrganizationService
|
|||||||
var enoughSeatsAvailable = true;
|
var enoughSeatsAvailable = true;
|
||||||
if (organization.Seats.HasValue)
|
if (organization.Seats.HasValue)
|
||||||
{
|
{
|
||||||
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organizationId);
|
var occupiedSeats = await GetOccupiedSeatCount(organization);
|
||||||
seatsAvailable = organization.Seats.Value - userCount;
|
seatsAvailable = organization.Seats.Value - occupiedSeats;
|
||||||
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
|
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2324,6 +2323,14 @@ public class OrganizationService : IOrganizationService
|
|||||||
throw new BadRequestException("Only owners can restore other owners.");
|
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);
|
await CheckPoliciesBeforeRestoreAsync(organizationUser, userService);
|
||||||
|
|
||||||
var status = GetPriorActiveOrganizationUserStatusType(organizationUser);
|
var status = GetPriorActiveOrganizationUserStatusType(organizationUser);
|
||||||
@ -2345,6 +2352,12 @@ public class OrganizationService : IOrganizationService
|
|||||||
throw new BadRequestException("Users invalid.");
|
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;
|
var deletingUserIsOwner = false;
|
||||||
if (restoringUserId.HasValue)
|
if (restoringUserId.HasValue)
|
||||||
{
|
{
|
||||||
@ -2455,4 +2468,10 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
return status;
|
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, 100, 100, true, "")]
|
||||||
[BitAutoData(0, null, 100, true, "")]
|
[BitAutoData(0, null, 100, true, "")]
|
||||||
[BitAutoData(1, 100, null, 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,
|
public void CanScale(int seatsToAdd, int? currentSeats, int? maxAutoscaleSeats,
|
||||||
bool expectedResult, string expectedFailureMessage, Organization organization,
|
bool expectedResult, string expectedFailureMessage, Organization organization,
|
||||||
SutProvider<OrganizationService> sutProvider)
|
SutProvider<OrganizationService> sutProvider)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user