mirror of
https://github.com/bitwarden/server.git
synced 2025-05-25 05:21:03 -05:00
Resolve the comment regarding abstraction
This commit is contained in:
parent
f62a3e2735
commit
3206b9aee5
@ -501,7 +501,7 @@ public class AccountController : Controller
|
||||
{
|
||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var initialSeatCount = organization.Seats.Value;
|
||||
var availableSeats = initialSeatCount - occupiedSeats;
|
||||
var availableSeats = initialSeatCount - occupiedSeats.Total;
|
||||
if (availableSeats < 1)
|
||||
{
|
||||
try
|
||||
|
@ -87,13 +87,14 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
||||
new InviteOrganizationUsersResponse(request.InviteOrganization.OrganizationId)));
|
||||
}
|
||||
|
||||
var seatCounts = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId);
|
||||
var validationResult = await inviteUsersValidator.ValidateAsync(new InviteOrganizationUsersValidationRequest
|
||||
{
|
||||
Invites = invitesToSend.ToArray(),
|
||||
InviteOrganization = request.InviteOrganization,
|
||||
PerformedBy = request.PerformedBy,
|
||||
PerformedAt = request.PerformedAt,
|
||||
OccupiedPmSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId),
|
||||
OccupiedPmSeats = seatCounts.Total,
|
||||
OccupiedSmSeats = await organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId)
|
||||
});
|
||||
|
||||
|
@ -66,8 +66,8 @@ public class RestoreOrganizationUserCommand(
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationUser.OrganizationId);
|
||||
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
||||
var seatCounts = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - seatCounts.Total;
|
||||
|
||||
if (availableSeats < 1)
|
||||
{
|
||||
@ -159,8 +159,8 @@ public class RestoreOrganizationUserCommand(
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
||||
var seatCounts = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - seatCounts.Total;
|
||||
var newSeatsRequired = organizationUserIds.Count() - availableSeats;
|
||||
await organizationService.AutoAddSeatsAsync(organization, newSeatsRequired);
|
||||
|
||||
|
@ -27,7 +27,7 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The ID of the organization to get the occupied seat count for.</param>
|
||||
/// <returns>The number of occupied seats for the organization.</returns>
|
||||
Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId);
|
||||
Task<OrganizationSeatCounts> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId);
|
||||
Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers);
|
||||
Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId);
|
||||
Task<Tuple<OrganizationUser?, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id);
|
||||
|
@ -342,15 +342,12 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
if (!organization.Seats.HasValue || organization.Seats.Value > newSeatTotal)
|
||||
{
|
||||
var totalConsumedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var organizationUsers = await _organizationUserRepository.GetManyByOrganizationAsync(organization.Id, null);
|
||||
var organizationUserOccupiedSeats = organizationUsers.Where(user => user.Status >= 0).Count();
|
||||
var sponsoredFamiliesOccupiedSeats = totalConsumedSeats - organizationUserOccupiedSeats;
|
||||
var seatCounts = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
|
||||
if (totalConsumedSeats > newSeatTotal)
|
||||
if (seatCounts.Total > newSeatTotal)
|
||||
{
|
||||
throw new BadRequestException($"Your organization has {organizationUserOccupiedSeats} members and {sponsoredFamiliesOccupiedSeats} sponsored families. " +
|
||||
$"To decrease the seat count below {totalConsumedSeats}, you must remove members or sponsorships.");
|
||||
throw new BadRequestException($"Your organization has {seatCounts.Users} members and {seatCounts.Sponsored} sponsored families. " +
|
||||
$"To decrease the seat count below {seatCounts.Total}, you must remove members or sponsorships.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -846,8 +843,8 @@ public class OrganizationService : IOrganizationService
|
||||
var newSeatsRequired = 0;
|
||||
if (organization.Seats.HasValue)
|
||||
{
|
||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.Value - occupiedSeats;
|
||||
var seatCounts = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.Value - seatCounts.Total;
|
||||
newSeatsRequired = invites.Sum(i => i.invite.Emails.Count()) - existingEmails.Count() - availableSeats;
|
||||
}
|
||||
|
||||
@ -1303,8 +1300,8 @@ public class OrganizationService : IOrganizationService
|
||||
var enoughSeatsAvailable = true;
|
||||
if (organization.Seats.HasValue)
|
||||
{
|
||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
seatsAvailable = organization.Seats.Value - occupiedSeats;
|
||||
var seatCounts = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
seatsAvailable = organization.Seats.Value - seatCounts.Total;
|
||||
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
public class OrganizationSeatCounts
|
||||
{
|
||||
public int Users { get; set; }
|
||||
public int Sponsored { get; set; }
|
||||
public int Total => Users + Sponsored;
|
||||
}
|
@ -89,8 +89,8 @@ public class CreateSponsorshipCommand(
|
||||
|
||||
if (isAdminInitiated && sponsoringOrganization.Seats.HasValue)
|
||||
{
|
||||
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrganization.Id);
|
||||
var availableSeats = sponsoringOrganization.Seats.Value - occupiedSeats;
|
||||
var seatCounts = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrganization.Id);
|
||||
var availableSeats = sponsoringOrganization.Seats.Value - seatCounts.Total;
|
||||
|
||||
if (availableSeats <= 0)
|
||||
{
|
||||
|
@ -117,12 +117,12 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
||||
(newPlan.PasswordManager.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));
|
||||
if (!organization.Seats.HasValue || organization.Seats.Value > updatedPasswordManagerSeats)
|
||||
{
|
||||
var occupiedSeats =
|
||||
var seatCounts =
|
||||
await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
if (occupiedSeats > updatedPasswordManagerSeats)
|
||||
if (seatCounts.Total > updatedPasswordManagerSeats)
|
||||
{
|
||||
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
|
||||
$"Your new plan only has ({updatedPasswordManagerSeats}) seats. Remove some users.");
|
||||
throw new BadRequestException($"Your organization has {seatCounts.Users} members and {seatCounts.Sponsored} sponsored families. " +
|
||||
$"To decrease the seat count below {seatCounts.Total}, you must remove members or sponsorships.");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,16 +88,16 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId)
|
||||
public async Task<OrganizationSeatCounts> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var result = await connection.ExecuteScalarAsync<int>(
|
||||
var result = await connection.QueryFirstOrDefaultAsync<OrganizationSeatCounts>(
|
||||
"[dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]",
|
||||
new { OrganizationId = organizationId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return result;
|
||||
return result ?? new OrganizationSeatCounts();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -227,10 +227,28 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
return await GetCountFromQuery(query);
|
||||
}
|
||||
|
||||
public async Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId)
|
||||
public async Task<OrganizationSeatCounts> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId)
|
||||
{
|
||||
var query = new OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery(organizationId);
|
||||
return await GetCountFromQuery(query);
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var users = await dbContext.OrganizationUsers
|
||||
.Where(ou => ou.OrganizationId == organizationId && ou.Status >= 0)
|
||||
.CountAsync();
|
||||
|
||||
var sponsored = await dbContext.OrganizationSponsorships
|
||||
.Where(os => os.SponsoringOrganizationId == organizationId &&
|
||||
os.IsAdminInitiated &&
|
||||
(os.ToDelete == false || (os.ToDelete == true && os.ValidUntil != null && os.ValidUntil > DateTime.UtcNow)) &&
|
||||
(os.SponsoredOrganizationId == null || (os.SponsoredOrganizationId != null && (os.ValidUntil == null || os.ValidUntil > DateTime.UtcNow))))
|
||||
.CountAsync();
|
||||
|
||||
return new OrganizationSeatCounts
|
||||
{
|
||||
Users = users,
|
||||
Sponsored = sponsored
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> GetCountByOrganizationIdAsync(Guid organizationId)
|
||||
|
@ -476,7 +476,11 @@ public class InviteOrganizationUserCommandTests
|
||||
orgUserRepository
|
||||
.GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)
|
||||
.Returns([ownerDetails]);
|
||||
orgUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(1);
|
||||
orgUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
orgUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id).Returns(1);
|
||||
|
||||
var orgRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
|
@ -5,6 +5,7 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -171,7 +172,11 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
// Setup for checking available seats
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id)
|
||||
.Returns(0);
|
||||
.Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 0
|
||||
});
|
||||
|
||||
|
||||
await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
|
||||
@ -320,7 +325,11 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
// Setup for checking available seats - organization has plenty of seats
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id)
|
||||
.Returns(5);
|
||||
.Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 5
|
||||
});
|
||||
|
||||
var actual = await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
|
||||
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, notes);
|
||||
@ -380,7 +389,11 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
// Setup for checking available seats - organization has no available seats
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id)
|
||||
.Returns(10);
|
||||
.Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 10
|
||||
});
|
||||
|
||||
// Setup for checking if can scale
|
||||
sutProvider.GetDependency<IOrganizationService>()
|
||||
@ -445,7 +458,11 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
// Setup for checking available seats - organization has no available seats
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id)
|
||||
.Returns(10);
|
||||
.Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 10
|
||||
});
|
||||
|
||||
// Setup for checking if can scale - cannot scale
|
||||
var failureReason = "Seat limit has been reached.";
|
||||
|
@ -0,0 +1,72 @@
|
||||
IF OBJECT_ID('[dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]
|
||||
@OrganizationId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
(
|
||||
-- Count organization users
|
||||
SELECT COUNT(1)
|
||||
FROM [dbo].[OrganizationUserView]
|
||||
WHERE OrganizationId = @OrganizationId
|
||||
AND Status >= 0 --Invited
|
||||
) as Users,
|
||||
(
|
||||
-- Count admin-initiated sponsorships towards the seat count
|
||||
-- Introduced in https://bitwarden.atlassian.net/browse/PM-17772
|
||||
SELECT COUNT(1)
|
||||
FROM [dbo].[OrganizationSponsorship]
|
||||
WHERE SponsoringOrganizationId = @OrganizationId
|
||||
AND IsAdminInitiated = 1
|
||||
AND (
|
||||
-- Not marked for deletion - always count
|
||||
(ToDelete = 0)
|
||||
OR
|
||||
-- Marked for deletion but has a valid until date in the future (RevokeWhenExpired status)
|
||||
(ToDelete = 1 AND ValidUntil IS NOT NULL AND ValidUntil > GETUTCDATE())
|
||||
)
|
||||
AND (
|
||||
-- SENT status: When SponsoredOrganizationId is null
|
||||
SponsoredOrganizationId IS NULL
|
||||
OR
|
||||
-- ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future
|
||||
(SponsoredOrganizationId IS NOT NULL AND (ValidUntil IS NULL OR ValidUntil > GETUTCDATE()))
|
||||
)
|
||||
) as Sponsored,
|
||||
(
|
||||
-- Count organization users
|
||||
SELECT COUNT(1)
|
||||
FROM [dbo].[OrganizationUserView]
|
||||
WHERE OrganizationId = @OrganizationId
|
||||
AND Status >= 0 --Invited
|
||||
) +
|
||||
(
|
||||
-- Count admin-initiated sponsorships towards the seat count
|
||||
SELECT COUNT(1)
|
||||
FROM [dbo].[OrganizationSponsorship]
|
||||
WHERE SponsoringOrganizationId = @OrganizationId
|
||||
AND IsAdminInitiated = 1
|
||||
AND (
|
||||
-- Not marked for deletion - always count
|
||||
(ToDelete = 0)
|
||||
OR
|
||||
-- Marked for deletion but has a valid until date in the future (RevokeWhenExpired status)
|
||||
(ToDelete = 1 AND ValidUntil IS NOT NULL AND ValidUntil > GETUTCDATE())
|
||||
)
|
||||
AND (
|
||||
-- SENT status: When SponsoredOrganizationId is null
|
||||
SponsoredOrganizationId IS NULL
|
||||
OR
|
||||
-- ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future
|
||||
(SponsoredOrganizationId IS NOT NULL AND (ValidUntil IS NULL OR ValidUntil > GETUTCDATE()))
|
||||
)
|
||||
) as Total
|
||||
END
|
||||
GO
|
Loading…
x
Reference in New Issue
Block a user