diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IGetOrganizationUsersManagementStatusQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IGetOrganizationUsersManagementStatusQuery.cs
index 694b44dd7817..1b73ebabf2bc 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IGetOrganizationUsersManagementStatusQuery.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IGetOrganizationUsersManagementStatusQuery.cs
@@ -10,6 +10,7 @@ public interface IGetOrganizationUsersManagementStatusQuery
///
/// A managed user is a user whose email domain matches one of the Organization's verified domains.
/// The organization must be enabled and be on an Enterprise plan.
+ /// The user must be a member of the organization with a confirmed or revoked status.
///
///
/// A dictionary containing the OrganizationUserId and a boolean indicating if the user is managed by the organization.
diff --git a/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs
index 5b274d3f88af..b39d7dd51f56 100644
--- a/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs
+++ b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs
@@ -19,7 +19,12 @@ public interface IOrganizationRepository : IRepository
Task> GetOwnerEmailAddressesById(Guid organizationId);
///
- /// Gets the organizations that have a verified domain matching the user's email domain.
+ /// Retrieves organizations where the user's email domain matches a verified organization domain.
///
+ ///
+ /// Only returns organizations where:
+ /// 1. The domain in the user's email matches a verified organization domain
+ /// 2. The user is an existing organization member with either 'Confirmed' or 'Revoked' status
+ ///
Task> GetByVerifiedUserEmailDomainAsync(Guid userId);
}
diff --git a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs
index a3a68b5de264..7b68e8a27359 100644
--- a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs
+++ b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs
@@ -57,5 +57,8 @@ UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId,
///
/// Returns a list of OrganizationUsers with email domains that match one of the Organization's claimed domains.
///
+ ///
+ /// Only returns users with a confirmed or revoked status.
+ ///
Task> GetManyByOrganizationWithClaimedDomainsAsync(Guid organizationId);
}
diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs
index 65bec5ea9f78..ca4eef07fc8c 100644
--- a/src/Core/Services/IUserService.cs
+++ b/src/Core/Services/IUserService.cs
@@ -90,8 +90,10 @@ Task UpdatePasswordHash(User user, string newPassword,
/// Indicates if the user is managed by any organization.
///
///
- /// A user is considered managed by an organization if their email domain matches one of the verified domains of that organization, and the user is a member of it.
- /// The organization must be enabled and able to have verified domains.
+ /// A user is considered "managed" when they meet all these criteria:
+ /// 1. Their email domain matches a verified domain of an organization
+ /// 2. They are a member of that organization (with confirmed or revoked status)
+ /// 3. The organization is enabled and has domain verification capabilities
///
///
/// False if the Account Deprovisioning feature flag is disabled.
diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs
index b3ee25488961..a9b75da8ddff 100644
--- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs
+++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs
@@ -290,8 +290,9 @@ join ou in dbContext.OrganizationUsers on u.Id equals ou.UserId
join o in dbContext.Organizations on ou.OrganizationId equals o.Id
join od in dbContext.OrganizationDomains on ou.OrganizationId equals od.OrganizationId
where u.Id == userId
- && od.VerifiedDate != null
- && u.Email.ToLower().EndsWith("@" + od.DomainName.ToLower())
+ && (ou.Status == OrganizationUserStatusType.Confirmed || ou.Status == OrganizationUserStatusType.Revoked)
+ && od.VerifiedDate != null
+ && u.Email.ToLower().EndsWith("@" + od.DomainName.ToLower())
select o;
return await query.ToArrayAsync();
diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserReadByClaimedOrganizationDomainsQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserReadByClaimedOrganizationDomainsQuery.cs
index d328691df0d2..d102f0ff5f01 100644
--- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserReadByClaimedOrganizationDomainsQuery.cs
+++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserReadByClaimedOrganizationDomainsQuery.cs
@@ -1,4 +1,5 @@
using Bit.Core.Entities;
+using Bit.Core.Enums;
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
@@ -16,8 +17,9 @@ public IQueryable Run(DatabaseContext dbContext)
var query = from ou in dbContext.OrganizationUsers
join u in dbContext.Users on ou.UserId equals u.Id
where ou.OrganizationId == _organizationId
- && dbContext.OrganizationDomains
- .Any(od => od.OrganizationId == _organizationId &&
+ && (ou.Status == OrganizationUserStatusType.Confirmed || ou.Status == OrganizationUserStatusType.Revoked)
+ && dbContext.OrganizationDomains
+ .Any(od => od.OrganizationId == _organizationId &&
od.VerifiedDate != null &&
u.Email.ToLower().EndsWith("@" + od.DomainName.ToLower()))
select ou;
diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByOrganizationIdWithClaimedDomains.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByOrganizationIdWithClaimedDomains.sql
index bb10a1a4810b..7e00ffcb8791 100644
--- a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByOrganizationIdWithClaimedDomains.sql
+++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByOrganizationIdWithClaimedDomains.sql
@@ -8,6 +8,10 @@ BEGIN
FROM [dbo].[OrganizationUserView] OU
INNER JOIN [dbo].[UserView] U ON OU.[UserId] = U.[Id]
WHERE OU.[OrganizationId] = @OrganizationId
+ AND (
+ OU.[Status] = 2 -- Confirmed
+ OR OU.[Status] = -1 -- Revoked
+ )
AND EXISTS (
SELECT 1
FROM [dbo].[OrganizationDomainView] OD
diff --git a/src/Sql/dbo/Stored Procedures/Organization_ReadByClaimedUserEmailDomain.sql b/src/Sql/dbo/Stored Procedures/Organization_ReadByClaimedUserEmailDomain.sql
index 39cf5d384c9c..a6caf2747404 100644
--- a/src/Sql/dbo/Stored Procedures/Organization_ReadByClaimedUserEmailDomain.sql
+++ b/src/Sql/dbo/Stored Procedures/Organization_ReadByClaimedUserEmailDomain.sql
@@ -10,6 +10,10 @@ BEGIN
INNER JOIN [dbo].[OrganizationView] O ON OU.[OrganizationId] = O.[Id]
INNER JOIN [dbo].[OrganizationDomainView] OD ON OU.[OrganizationId] = OD.[OrganizationId]
WHERE U.[Id] = @UserId
+ AND (
+ OU.[Status] = 2 -- Confirmed
+ OR OU.[Status] = -1 -- Revoked
+ )
AND OD.[VerifiedDate] IS NOT NULL
AND U.[Email] LIKE '%@' + OD.[DomainName];
END
diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs
index f6dc4a989d8e..dff25e5391f2 100644
--- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs
+++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs
@@ -253,4 +253,80 @@ public async Task GetByVerifiedUserEmailDomainAsync_WithNonExistentUser_ReturnsE
Assert.Empty(result);
}
+
+ [DatabaseTheory, DatabaseData]
+ public async Task GetByClaimedUserDomainAsync_WithNonConfirmedOrRevokedUsers_ReturnsEmpty(
+ IUserRepository userRepository,
+ IOrganizationRepository organizationRepository,
+ IOrganizationUserRepository organizationUserRepository,
+ IOrganizationDomainRepository organizationDomainRepository)
+ {
+ var id = Guid.NewGuid();
+ var domainName = $"{id}.example.com";
+
+ var user1 = await userRepository.CreateAsync(new User
+ {
+ Name = "Test User 1",
+ Email = $"test+{id}@{domainName}",
+ ApiKey = "TEST",
+ SecurityStamp = "stamp",
+ Kdf = KdfType.PBKDF2_SHA256,
+ KdfIterations = 1,
+ KdfMemory = 2,
+ KdfParallelism = 3
+ });
+
+ var user2 = await userRepository.CreateAsync(new User
+ {
+ Name = "Test User 2",
+ Email = $"test+{id}@x-{domainName}", // Different domain
+ ApiKey = "TEST",
+ SecurityStamp = "stamp",
+ Kdf = KdfType.PBKDF2_SHA256,
+ KdfIterations = 1,
+ KdfMemory = 2,
+ KdfParallelism = 3
+ });
+
+ var organization = await organizationRepository.CreateAsync(new Organization
+ {
+ Name = $"Test Org {id}",
+ BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULl
+ Plan = "Test", // TODO: EF does not enforce this being NOT NULl
+ PrivateKey = "privatekey",
+ });
+
+ var organizationDomain = new OrganizationDomain
+ {
+ OrganizationId = organization.Id,
+ DomainName = domainName,
+ Txt = "btw+12345",
+ };
+ organizationDomain.SetVerifiedDate();
+ organizationDomain.SetNextRunDate(12);
+ organizationDomain.SetJobRunCount();
+ await organizationDomainRepository.CreateAsync(organizationDomain);
+
+ await organizationUserRepository.CreateAsync(new OrganizationUser
+ {
+ OrganizationId = organization.Id,
+ UserId = user1.Id,
+ Status = OrganizationUserStatusType.Invited,
+ ResetPasswordKey = "resetpasswordkey1",
+ });
+
+ await organizationUserRepository.CreateAsync(new OrganizationUser
+ {
+ OrganizationId = organization.Id,
+ UserId = user2.Id,
+ Status = OrganizationUserStatusType.Accepted,
+ ResetPasswordKey = "resetpasswordkey1",
+ });
+
+ var user1Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user1.Id);
+ var user2Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user2.Id);
+
+ Assert.Empty(user1Response);
+ Assert.Empty(user2Response);
+ }
}
diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs
index dba511074eb7..0de0ecfe4b4d 100644
--- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs
+++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs
@@ -355,4 +355,79 @@ await organizationUserRepository.CreateAsync(new OrganizationUser
Assert.Single(responseModel);
Assert.Equal(orgUser1.Id, responseModel.Single().Id);
}
+
+ [DatabaseTheory, DatabaseData]
+ public async Task GetManyByOrganizationWithClaimedDomainsAsync_WithNonConfirmedOrRevokedUsers_ReturnsEmpty(
+ IUserRepository userRepository,
+ IOrganizationRepository organizationRepository,
+ IOrganizationUserRepository organizationUserRepository,
+ IOrganizationDomainRepository organizationDomainRepository)
+ {
+ var id = Guid.NewGuid();
+ var domainName = $"{id}.example.com";
+
+ var user1 = await userRepository.CreateAsync(new User
+ {
+ Name = "Test User 1",
+ Email = $"test+{id}@{domainName}",
+ ApiKey = "TEST",
+ SecurityStamp = "stamp",
+ Kdf = KdfType.PBKDF2_SHA256,
+ KdfIterations = 1,
+ KdfMemory = 2,
+ KdfParallelism = 3
+ });
+
+ var user2 = await userRepository.CreateAsync(new User
+ {
+ Name = "Test User 2",
+ Email = $"test+{id}@x-{domainName}", // Different domain
+ ApiKey = "TEST",
+ SecurityStamp = "stamp",
+ Kdf = KdfType.PBKDF2_SHA256,
+ KdfIterations = 1,
+ KdfMemory = 2,
+ KdfParallelism = 3
+ });
+
+ var organization = await organizationRepository.CreateAsync(new Organization
+ {
+ Name = $"Test Org {id}",
+ BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULl
+ Plan = "Test", // TODO: EF does not enforce this being NOT NULl
+ PrivateKey = "privatekey",
+ });
+
+ var organizationDomain = new OrganizationDomain
+ {
+ OrganizationId = organization.Id,
+ DomainName = domainName,
+ Txt = "btw+12345",
+ };
+ organizationDomain.SetVerifiedDate();
+ organizationDomain.SetNextRunDate(12);
+ organizationDomain.SetJobRunCount();
+ await organizationDomainRepository.CreateAsync(organizationDomain);
+
+ var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser
+ {
+ OrganizationId = organization.Id,
+ UserId = user1.Id,
+ Status = OrganizationUserStatusType.Invited,
+ ResetPasswordKey = "resetpasswordkey1",
+ });
+
+ await organizationUserRepository.CreateAsync(new OrganizationUser
+ {
+ OrganizationId = organization.Id,
+ UserId = user2.Id,
+ Status = OrganizationUserStatusType.Accepted,
+ ResetPasswordKey = "resetpasswordkey1",
+ });
+
+ var responseModel = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organization.Id);
+
+ Assert.NotNull(responseModel);
+ Assert.Empty(responseModel);
+ }
}
diff --git a/util/Migrator/DbScripts/2024-11-05_00_UsersManagedByOrgConfirmedRevoked.sql b/util/Migrator/DbScripts/2024-11-05_00_UsersManagedByOrgConfirmedRevoked.sql
new file mode 100644
index 000000000000..cff5c1ce24dd
--- /dev/null
+++ b/util/Migrator/DbScripts/2024-11-05_00_UsersManagedByOrgConfirmedRevoked.sql
@@ -0,0 +1,44 @@
+CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_ReadByOrganizationIdWithClaimedDomains]
+ @OrganizationId UNIQUEIDENTIFIER
+AS
+BEGIN
+ SET NOCOUNT ON;
+
+ SELECT OU.*
+ FROM [dbo].[OrganizationUserView] OU
+ INNER JOIN [dbo].[UserView] U ON OU.[UserId] = U.[Id]
+ WHERE OU.[OrganizationId] = @OrganizationId
+ AND (
+ OU.[Status] = 2 -- Confirmed
+ OR OU.[Status] = -1 -- Revoked
+ )
+ AND EXISTS (
+ SELECT 1
+ FROM [dbo].[OrganizationDomainView] OD
+ WHERE OD.[OrganizationId] = @OrganizationId
+ AND OD.[VerifiedDate] IS NOT NULL
+ AND U.[Email] LIKE '%@' + OD.[DomainName]
+ );
+END
+GO
+
+CREATE OR ALTER PROCEDURE [dbo].[Organization_ReadByClaimedUserEmailDomain]
+ @UserId UNIQUEIDENTIFIER
+AS
+BEGIN
+ SET NOCOUNT ON;
+
+ SELECT O.*
+ FROM [dbo].[UserView] U
+ INNER JOIN [dbo].[OrganizationUserView] OU ON U.[Id] = OU.[UserId]
+ INNER JOIN [dbo].[OrganizationView] O ON OU.[OrganizationId] = O.[Id]
+ INNER JOIN [dbo].[OrganizationDomainView] OD ON OU.[OrganizationId] = OD.[OrganizationId]
+ WHERE U.[Id] = @UserId
+ AND (
+ OU.[Status] = 2 -- Confirmed
+ OR OU.[Status] = -1 -- Revoked
+ )
+ AND OD.[VerifiedDate] IS NOT NULL
+ AND U.[Email] LIKE '%@' + OD.[DomainName];
+END
+GO