From 7ebf312b844a637a0066bd9f926f2533b6b33d63 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Wed, 30 Apr 2025 07:19:28 -0400 Subject: [PATCH] Updated seat count logic to ensure that only the correct sponsorships are counted towards the seat count (#5711) --- ...dOccupiedSeatCountByOrganizationIdQuery.cs | 14 ++++++- ..._ReadOccupiedSeatCountByOrganizationId.sql | 14 +++++++ ...erReadOccupiedSeatCountForSponsorships.sql | 41 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 util/Migrator/DbScripts/2025-04-24_00_UpdateOrgUserReadOccupiedSeatCountForSponsorships.sql diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery.cs index 0a681e2b5f..6be51f2036 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery.cs @@ -23,7 +23,19 @@ public class OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery : IQuery var sponsorshipsQuery = from os in dbContext.OrganizationSponsorships where os.SponsoringOrganizationId == _organizationId && os.IsAdminInitiated && - !os.ToDelete + ( + // Not marked for deletion - always count + (!os.ToDelete) || + // Marked for deletion but has a valid until date in the future (RevokeWhenExpired status) + (os.ToDelete && os.ValidUntil.HasValue && os.ValidUntil.Value > DateTime.UtcNow) + ) && + ( + // SENT status: When SponsoredOrganizationId is null + os.SponsoredOrganizationId == null || + // ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future + (os.SponsoredOrganizationId != null && + (!os.ValidUntil.HasValue || os.ValidUntil.Value > DateTime.UtcNow)) + ) select new OrganizationUser { Id = os.Id, diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadOccupiedSeatCountByOrganizationId.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadOccupiedSeatCountByOrganizationId.sql index 933441a210..3d861670a6 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadOccupiedSeatCountByOrganizationId.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadOccupiedSeatCountByOrganizationId.sql @@ -19,5 +19,19 @@ BEGIN 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())) + ) ) END diff --git a/util/Migrator/DbScripts/2025-04-24_00_UpdateOrgUserReadOccupiedSeatCountForSponsorships.sql b/util/Migrator/DbScripts/2025-04-24_00_UpdateOrgUserReadOccupiedSeatCountForSponsorships.sql new file mode 100644 index 0000000000..7260c9c6d4 --- /dev/null +++ b/util/Migrator/DbScripts/2025-04-24_00_UpdateOrgUserReadOccupiedSeatCountForSponsorships.sql @@ -0,0 +1,41 @@ +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 + ( + SELECT COUNT(1) + FROM [dbo].[OrganizationUserView] + WHERE OrganizationId = @OrganizationId + AND Status >= 0 --Invited + ) + + ( + 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())) + ) + ) +END +GO