mirror of
https://github.com/bitwarden/server.git
synced 2026-01-31 06:03:12 +08:00
[PM-22236] Fix invited accounts stuck in intermediate claimed status (#6810)
* Exclude invited users from claimed domain checks. These users should be excluded by the JOIN on UserId, but it's a known issue that some invited users have this FK set.
This commit is contained in:
@@ -21,7 +21,9 @@ public interface IOrganizationRepository : IRepository<Organization, Guid>
|
|||||||
Task<IEnumerable<string>> GetOwnerEmailAddressesById(Guid organizationId);
|
Task<IEnumerable<string>> GetOwnerEmailAddressesById(Guid organizationId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the organizations that have a verified domain matching the user's email domain.
|
/// Gets the organizations that have claimed the user's account. Currently, only one organization may claim a user.
|
||||||
|
/// This requires that the organization has claimed the user's domain and the user is an organization member.
|
||||||
|
/// It excludes invited members.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId);
|
Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId);
|
||||||
|
|
||||||
|
|||||||
@@ -325,7 +325,8 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
|||||||
where ou.UserId == userWithDomain.UserId &&
|
where ou.UserId == userWithDomain.UserId &&
|
||||||
od.DomainName == userWithDomain.EmailDomain &&
|
od.DomainName == userWithDomain.EmailDomain &&
|
||||||
od.VerifiedDate != null &&
|
od.VerifiedDate != null &&
|
||||||
o.Enabled == true
|
o.Enabled == true &&
|
||||||
|
ou.Status != OrganizationUserStatusType.Invited
|
||||||
select o;
|
select o;
|
||||||
|
|
||||||
return await query.ToArrayAsync();
|
return await query.ToArrayAsync();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ public class OrganizationUserReadByClaimedOrganizationDomainsQuery : IQuery<Orga
|
|||||||
var query = from ou in dbContext.OrganizationUsers
|
var query = from ou in dbContext.OrganizationUsers
|
||||||
join u in dbContext.Users on ou.UserId equals u.Id
|
join u in dbContext.Users on ou.UserId equals u.Id
|
||||||
where ou.OrganizationId == _organizationId
|
where ou.OrganizationId == _organizationId
|
||||||
|
&& ou.Status != OrganizationUserStatusType.Invited
|
||||||
&& dbContext.OrganizationDomains
|
&& dbContext.OrganizationDomains
|
||||||
.Any(od => od.OrganizationId == _organizationId &&
|
.Any(od => od.OrganizationId == _organizationId &&
|
||||||
od.VerifiedDate != null &&
|
od.VerifiedDate != null &&
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ BEGIN
|
|||||||
SELECT *
|
SELECT *
|
||||||
FROM [dbo].[OrganizationUserView]
|
FROM [dbo].[OrganizationUserView]
|
||||||
WHERE [OrganizationId] = @OrganizationId
|
WHERE [OrganizationId] = @OrganizationId
|
||||||
|
AND [Status] != 0 -- Exclude invited users
|
||||||
),
|
),
|
||||||
UserDomains AS (
|
UserDomains AS (
|
||||||
SELECT U.[Id], U.[EmailDomain]
|
SELECT U.[Id], U.[EmailDomain]
|
||||||
FROM [dbo].[UserEmailDomainView] U
|
FROM [dbo].[UserEmailDomainView] U
|
||||||
WHERE EXISTS (
|
WHERE EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM [dbo].[OrganizationDomainView] OD
|
FROM [dbo].[OrganizationDomainView] OD
|
||||||
WHERE OD.[OrganizationId] = @OrganizationId
|
WHERE OD.[OrganizationId] = @OrganizationId
|
||||||
AND OD.[VerifiedDate] IS NOT NULL
|
AND OD.[VerifiedDate] IS NOT NULL
|
||||||
AND OD.[DomainName] = U.[EmailDomain]
|
AND OD.[DomainName] = U.[EmailDomain]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ BEGIN
|
|||||||
|
|
||||||
WITH CTE_User AS (
|
WITH CTE_User AS (
|
||||||
SELECT
|
SELECT
|
||||||
U.*,
|
U.[Id],
|
||||||
SUBSTRING(U.Email, CHARINDEX('@', U.Email) + 1, LEN(U.Email)) AS EmailDomain
|
SUBSTRING(U.Email, CHARINDEX('@', U.Email) + 1, LEN(U.Email)) AS EmailDomain
|
||||||
FROM dbo.[UserView] U
|
FROM dbo.[UserView] U
|
||||||
WHERE U.[Id] = @UserId
|
WHERE U.[Id] = @UserId
|
||||||
@@ -19,4 +19,5 @@ BEGIN
|
|||||||
WHERE OD.[VerifiedDate] IS NOT NULL
|
WHERE OD.[VerifiedDate] IS NOT NULL
|
||||||
AND CU.EmailDomain = OD.[DomainName]
|
AND CU.EmailDomain = OD.[DomainName]
|
||||||
AND O.[Enabled] = 1
|
AND O.[Enabled] = 1
|
||||||
|
AND OU.[Status] != 0 -- Exclude invited users
|
||||||
END
|
END
|
||||||
|
|||||||
@@ -0,0 +1,335 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.OrganizationRepository;
|
||||||
|
|
||||||
|
public class GetByVerifiedUserEmailDomainAsyncTests
|
||||||
|
{
|
||||||
|
[Theory, DatabaseData]
|
||||||
|
public async Task GetByClaimedUserDomainAsync_WithVerifiedDomain_Success(
|
||||||
|
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 user3 = await userRepository.CreateAsync(new User
|
||||||
|
{
|
||||||
|
Name = "Test User 2",
|
||||||
|
Email = $"test+{id}@{domainName}.example.com", // Different domain
|
||||||
|
ApiKey = "TEST",
|
||||||
|
SecurityStamp = "stamp",
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = 1,
|
||||||
|
KdfMemory = 2,
|
||||||
|
KdfParallelism = 3
|
||||||
|
});
|
||||||
|
|
||||||
|
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||||
|
|
||||||
|
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.CreateConfirmedTestOrganizationUserAsync(organization, user1);
|
||||||
|
await organizationUserRepository.CreateConfirmedTestOrganizationUserAsync(organization, user2);
|
||||||
|
await organizationUserRepository.CreateConfirmedTestOrganizationUserAsync(organization, user3);
|
||||||
|
|
||||||
|
var user1Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user1.Id);
|
||||||
|
var user2Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user2.Id);
|
||||||
|
var user3Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user3.Id);
|
||||||
|
|
||||||
|
Assert.NotEmpty(user1Response);
|
||||||
|
Assert.Equal(organization.Id, user1Response.First().Id);
|
||||||
|
Assert.Empty(user2Response);
|
||||||
|
Assert.Empty(user3Response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, DatabaseData]
|
||||||
|
public async Task GetByVerifiedUserEmailDomainAsync_WithUnverifiedDomains_ReturnsEmpty(
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IOrganizationDomainRepository organizationDomainRepository)
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
var domainName = $"{id}.example.com";
|
||||||
|
|
||||||
|
var user = await userRepository.CreateAsync(new User
|
||||||
|
{
|
||||||
|
Name = "Test User",
|
||||||
|
Email = $"test+{id}@{domainName}",
|
||||||
|
ApiKey = "TEST",
|
||||||
|
SecurityStamp = "stamp",
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = 1,
|
||||||
|
KdfMemory = 2,
|
||||||
|
KdfParallelism = 3
|
||||||
|
});
|
||||||
|
|
||||||
|
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||||
|
|
||||||
|
var organizationDomain = new OrganizationDomain
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
DomainName = domainName,
|
||||||
|
Txt = "btw+12345",
|
||||||
|
};
|
||||||
|
organizationDomain.SetNextRunDate(12);
|
||||||
|
organizationDomain.SetJobRunCount();
|
||||||
|
await organizationDomainRepository.CreateAsync(organizationDomain);
|
||||||
|
|
||||||
|
await organizationUserRepository.CreateConfirmedTestOrganizationUserAsync(organization, user);
|
||||||
|
|
||||||
|
var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user.Id);
|
||||||
|
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, DatabaseData]
|
||||||
|
public async Task GetByVerifiedUserEmailDomainAsync_WithMultipleVerifiedDomains_ReturnsAllMatchingOrganizations(
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IOrganizationDomainRepository organizationDomainRepository)
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
var domainName = $"{id}.example.com";
|
||||||
|
|
||||||
|
var user = await userRepository.CreateAsync(new User
|
||||||
|
{
|
||||||
|
Name = "Test User",
|
||||||
|
Email = $"test+{id}@{domainName}",
|
||||||
|
ApiKey = "TEST",
|
||||||
|
SecurityStamp = "stamp",
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = 1,
|
||||||
|
KdfMemory = 2,
|
||||||
|
KdfParallelism = 3
|
||||||
|
});
|
||||||
|
|
||||||
|
var organization1 = await organizationRepository.CreateTestOrganizationAsync();
|
||||||
|
var organization2 = await organizationRepository.CreateTestOrganizationAsync();
|
||||||
|
|
||||||
|
var organizationDomain1 = new OrganizationDomain
|
||||||
|
{
|
||||||
|
OrganizationId = organization1.Id,
|
||||||
|
DomainName = domainName,
|
||||||
|
Txt = "btw+12345",
|
||||||
|
};
|
||||||
|
organizationDomain1.SetNextRunDate(12);
|
||||||
|
organizationDomain1.SetJobRunCount();
|
||||||
|
organizationDomain1.SetVerifiedDate();
|
||||||
|
await organizationDomainRepository.CreateAsync(organizationDomain1);
|
||||||
|
|
||||||
|
var organizationDomain2 = new OrganizationDomain
|
||||||
|
{
|
||||||
|
OrganizationId = organization2.Id,
|
||||||
|
DomainName = domainName,
|
||||||
|
Txt = "btw+67890",
|
||||||
|
};
|
||||||
|
organizationDomain2.SetNextRunDate(12);
|
||||||
|
organizationDomain2.SetJobRunCount();
|
||||||
|
organizationDomain2.SetVerifiedDate();
|
||||||
|
await organizationDomainRepository.CreateAsync(organizationDomain2);
|
||||||
|
|
||||||
|
await organizationUserRepository.CreateConfirmedTestOrganizationUserAsync(organization1, user);
|
||||||
|
await organizationUserRepository.CreateConfirmedTestOrganizationUserAsync(organization2, user);
|
||||||
|
|
||||||
|
var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user.Id);
|
||||||
|
|
||||||
|
Assert.Equal(2, result.Count);
|
||||||
|
Assert.Contains(result, org => org.Id == organization1.Id);
|
||||||
|
Assert.Contains(result, org => org.Id == organization2.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, DatabaseData]
|
||||||
|
public async Task GetByVerifiedUserEmailDomainAsync_WithNonExistentUser_ReturnsEmpty(
|
||||||
|
IOrganizationRepository organizationRepository)
|
||||||
|
{
|
||||||
|
var nonExistentUserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(nonExistentUserId);
|
||||||
|
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests an edge case where some invited users are created linked to a UserId.
|
||||||
|
/// This is defective behavior, but will take longer to fix - for now, we are defensive and expressly
|
||||||
|
/// exclude such users from the results without relying on the inner join only.
|
||||||
|
/// Invited-revoked users linked to a UserId remain intentionally unhandled for now as they have not caused
|
||||||
|
/// any issues to date and we want to minimize edge cases.
|
||||||
|
/// We will fix the underlying issue going forward: https://bitwarden.atlassian.net/browse/PM-22405
|
||||||
|
/// </summary>
|
||||||
|
[Theory, DatabaseData]
|
||||||
|
public async Task GetByVerifiedUserEmailDomainAsync_WithInvitedUserWithUserId_ReturnsEmpty(
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IOrganizationDomainRepository organizationDomainRepository)
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
var domainName = $"{id}.example.com";
|
||||||
|
|
||||||
|
var user = await userRepository.CreateAsync(new User
|
||||||
|
{
|
||||||
|
Name = "Test User",
|
||||||
|
Email = $"test+{id}@{domainName}",
|
||||||
|
ApiKey = "TEST",
|
||||||
|
SecurityStamp = "stamp",
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = 1,
|
||||||
|
KdfMemory = 2,
|
||||||
|
KdfParallelism = 3
|
||||||
|
});
|
||||||
|
|
||||||
|
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||||
|
|
||||||
|
var organizationDomain = new OrganizationDomain
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
DomainName = domainName,
|
||||||
|
Txt = "btw+12345",
|
||||||
|
};
|
||||||
|
organizationDomain.SetVerifiedDate();
|
||||||
|
organizationDomain.SetNextRunDate(12);
|
||||||
|
organizationDomain.SetJobRunCount();
|
||||||
|
await organizationDomainRepository.CreateAsync(organizationDomain);
|
||||||
|
|
||||||
|
// Create invited user with matching email domain but UserId set (edge case)
|
||||||
|
await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
UserId = user.Id,
|
||||||
|
Email = user.Email,
|
||||||
|
Status = OrganizationUserStatusType.Invited,
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user.Id);
|
||||||
|
|
||||||
|
// Invited users should be excluded even if they have UserId set
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, DatabaseData]
|
||||||
|
public async Task GetByVerifiedUserEmailDomainAsync_WithAcceptedUser_ReturnsOrganization(
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IOrganizationDomainRepository organizationDomainRepository)
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
var domainName = $"{id}.example.com";
|
||||||
|
|
||||||
|
var user = await userRepository.CreateAsync(new User
|
||||||
|
{
|
||||||
|
Name = "Test User",
|
||||||
|
Email = $"test+{id}@{domainName}",
|
||||||
|
ApiKey = "TEST",
|
||||||
|
SecurityStamp = "stamp",
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = 1,
|
||||||
|
KdfMemory = 2,
|
||||||
|
KdfParallelism = 3
|
||||||
|
});
|
||||||
|
|
||||||
|
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||||
|
|
||||||
|
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.CreateAcceptedTestOrganizationUserAsync(organization, user);
|
||||||
|
|
||||||
|
var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user.Id);
|
||||||
|
|
||||||
|
Assert.NotEmpty(result);
|
||||||
|
Assert.Equal(organization.Id, result.First().Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, DatabaseData]
|
||||||
|
public async Task GetByVerifiedUserEmailDomainAsync_WithRevokedUser_ReturnsOrganization(
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IOrganizationDomainRepository organizationDomainRepository)
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
var domainName = $"{id}.example.com";
|
||||||
|
|
||||||
|
var user = await userRepository.CreateAsync(new User
|
||||||
|
{
|
||||||
|
Name = "Test User",
|
||||||
|
Email = $"test+{id}@{domainName}",
|
||||||
|
ApiKey = "TEST",
|
||||||
|
SecurityStamp = "stamp",
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = 1,
|
||||||
|
KdfMemory = 2,
|
||||||
|
KdfParallelism = 3
|
||||||
|
});
|
||||||
|
|
||||||
|
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||||
|
|
||||||
|
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.CreateRevokedTestOrganizationUserAsync(organization, user);
|
||||||
|
|
||||||
|
var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user.Id);
|
||||||
|
|
||||||
|
Assert.NotEmpty(result);
|
||||||
|
Assert.Equal(organization.Id, result.First().Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,254 +8,7 @@ namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories;
|
|||||||
|
|
||||||
public class OrganizationRepositoryTests
|
public class OrganizationRepositoryTests
|
||||||
{
|
{
|
||||||
[DatabaseTheory, DatabaseData]
|
[Theory, DatabaseData]
|
||||||
public async Task GetByClaimedUserDomainAsync_WithVerifiedDomain_Success(
|
|
||||||
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 user3 = await userRepository.CreateAsync(new User
|
|
||||||
{
|
|
||||||
Name = "Test User 2",
|
|
||||||
Email = $"test+{id}@{domainName}.example.com", // 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.Confirmed,
|
|
||||||
ResetPasswordKey = "resetpasswordkey1",
|
|
||||||
});
|
|
||||||
|
|
||||||
await organizationUserRepository.CreateAsync(new OrganizationUser
|
|
||||||
{
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
UserId = user2.Id,
|
|
||||||
Status = OrganizationUserStatusType.Confirmed,
|
|
||||||
ResetPasswordKey = "resetpasswordkey1",
|
|
||||||
});
|
|
||||||
|
|
||||||
await organizationUserRepository.CreateAsync(new OrganizationUser
|
|
||||||
{
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
UserId = user3.Id,
|
|
||||||
Status = OrganizationUserStatusType.Confirmed,
|
|
||||||
ResetPasswordKey = "resetpasswordkey1",
|
|
||||||
});
|
|
||||||
|
|
||||||
var user1Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user1.Id);
|
|
||||||
var user2Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user2.Id);
|
|
||||||
var user3Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user3.Id);
|
|
||||||
|
|
||||||
Assert.NotEmpty(user1Response);
|
|
||||||
Assert.Equal(organization.Id, user1Response.First().Id);
|
|
||||||
Assert.Empty(user2Response);
|
|
||||||
Assert.Empty(user3Response);
|
|
||||||
}
|
|
||||||
|
|
||||||
[DatabaseTheory, DatabaseData]
|
|
||||||
public async Task GetByVerifiedUserEmailDomainAsync_WithUnverifiedDomains_ReturnsEmpty(
|
|
||||||
IUserRepository userRepository,
|
|
||||||
IOrganizationRepository organizationRepository,
|
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
|
||||||
IOrganizationDomainRepository organizationDomainRepository)
|
|
||||||
{
|
|
||||||
var id = Guid.NewGuid();
|
|
||||||
var domainName = $"{id}.example.com";
|
|
||||||
|
|
||||||
var user = await userRepository.CreateAsync(new User
|
|
||||||
{
|
|
||||||
Name = "Test User",
|
|
||||||
Email = $"test+{id}@{domainName}",
|
|
||||||
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 = user.Email,
|
|
||||||
Plan = "Test",
|
|
||||||
PrivateKey = "privatekey",
|
|
||||||
});
|
|
||||||
|
|
||||||
var organizationDomain = new OrganizationDomain
|
|
||||||
{
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
DomainName = domainName,
|
|
||||||
Txt = "btw+12345",
|
|
||||||
};
|
|
||||||
organizationDomain.SetNextRunDate(12);
|
|
||||||
organizationDomain.SetJobRunCount();
|
|
||||||
await organizationDomainRepository.CreateAsync(organizationDomain);
|
|
||||||
|
|
||||||
await organizationUserRepository.CreateAsync(new OrganizationUser
|
|
||||||
{
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
UserId = user.Id,
|
|
||||||
Status = OrganizationUserStatusType.Confirmed,
|
|
||||||
ResetPasswordKey = "resetpasswordkey",
|
|
||||||
});
|
|
||||||
|
|
||||||
var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user.Id);
|
|
||||||
|
|
||||||
Assert.Empty(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[DatabaseTheory, DatabaseData]
|
|
||||||
public async Task GetByVerifiedUserEmailDomainAsync_WithMultipleVerifiedDomains_ReturnsAllMatchingOrganizations(
|
|
||||||
IUserRepository userRepository,
|
|
||||||
IOrganizationRepository organizationRepository,
|
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
|
||||||
IOrganizationDomainRepository organizationDomainRepository)
|
|
||||||
{
|
|
||||||
var id = Guid.NewGuid();
|
|
||||||
var domainName = $"{id}.example.com";
|
|
||||||
|
|
||||||
var user = await userRepository.CreateAsync(new User
|
|
||||||
{
|
|
||||||
Name = "Test User",
|
|
||||||
Email = $"test+{id}@{domainName}",
|
|
||||||
ApiKey = "TEST",
|
|
||||||
SecurityStamp = "stamp",
|
|
||||||
Kdf = KdfType.PBKDF2_SHA256,
|
|
||||||
KdfIterations = 1,
|
|
||||||
KdfMemory = 2,
|
|
||||||
KdfParallelism = 3
|
|
||||||
});
|
|
||||||
|
|
||||||
var organization1 = await organizationRepository.CreateAsync(new Organization
|
|
||||||
{
|
|
||||||
Name = $"Test Org 1 {id}",
|
|
||||||
BillingEmail = user.Email,
|
|
||||||
Plan = "Test",
|
|
||||||
PrivateKey = "privatekey1",
|
|
||||||
});
|
|
||||||
|
|
||||||
var organization2 = await organizationRepository.CreateAsync(new Organization
|
|
||||||
{
|
|
||||||
Name = $"Test Org 2 {id}",
|
|
||||||
BillingEmail = user.Email,
|
|
||||||
Plan = "Test",
|
|
||||||
PrivateKey = "privatekey2",
|
|
||||||
});
|
|
||||||
|
|
||||||
var organizationDomain1 = new OrganizationDomain
|
|
||||||
{
|
|
||||||
OrganizationId = organization1.Id,
|
|
||||||
DomainName = domainName,
|
|
||||||
Txt = "btw+12345",
|
|
||||||
};
|
|
||||||
organizationDomain1.SetNextRunDate(12);
|
|
||||||
organizationDomain1.SetJobRunCount();
|
|
||||||
organizationDomain1.SetVerifiedDate();
|
|
||||||
await organizationDomainRepository.CreateAsync(organizationDomain1);
|
|
||||||
|
|
||||||
var organizationDomain2 = new OrganizationDomain
|
|
||||||
{
|
|
||||||
OrganizationId = organization2.Id,
|
|
||||||
DomainName = domainName,
|
|
||||||
Txt = "btw+67890",
|
|
||||||
};
|
|
||||||
organizationDomain2.SetNextRunDate(12);
|
|
||||||
organizationDomain2.SetJobRunCount();
|
|
||||||
organizationDomain2.SetVerifiedDate();
|
|
||||||
await organizationDomainRepository.CreateAsync(organizationDomain2);
|
|
||||||
|
|
||||||
await organizationUserRepository.CreateAsync(new OrganizationUser
|
|
||||||
{
|
|
||||||
OrganizationId = organization1.Id,
|
|
||||||
UserId = user.Id,
|
|
||||||
Status = OrganizationUserStatusType.Confirmed,
|
|
||||||
ResetPasswordKey = "resetpasswordkey1",
|
|
||||||
});
|
|
||||||
|
|
||||||
await organizationUserRepository.CreateAsync(new OrganizationUser
|
|
||||||
{
|
|
||||||
OrganizationId = organization2.Id,
|
|
||||||
UserId = user.Id,
|
|
||||||
Status = OrganizationUserStatusType.Confirmed,
|
|
||||||
ResetPasswordKey = "resetpasswordkey2",
|
|
||||||
});
|
|
||||||
|
|
||||||
var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user.Id);
|
|
||||||
|
|
||||||
Assert.Equal(2, result.Count);
|
|
||||||
Assert.Contains(result, org => org.Id == organization1.Id);
|
|
||||||
Assert.Contains(result, org => org.Id == organization2.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
[DatabaseTheory, DatabaseData]
|
|
||||||
public async Task GetByVerifiedUserEmailDomainAsync_WithNonExistentUser_ReturnsEmpty(
|
|
||||||
IOrganizationRepository organizationRepository)
|
|
||||||
{
|
|
||||||
var nonExistentUserId = Guid.NewGuid();
|
|
||||||
|
|
||||||
var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(nonExistentUserId);
|
|
||||||
|
|
||||||
Assert.Empty(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[DatabaseTheory, DatabaseData]
|
|
||||||
public async Task GetManyByIdsAsync_ExistingOrganizations_ReturnsOrganizations(IOrganizationRepository organizationRepository)
|
public async Task GetManyByIdsAsync_ExistingOrganizations_ReturnsOrganizations(IOrganizationRepository organizationRepository)
|
||||||
{
|
{
|
||||||
var email = "test@email.com";
|
var email = "test@email.com";
|
||||||
@@ -287,7 +40,7 @@ public class OrganizationRepositoryTests
|
|||||||
await organizationRepository.DeleteAsync(organization2);
|
await organizationRepository.DeleteAsync(organization2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[DatabaseTheory, DatabaseData]
|
[Theory, DatabaseData]
|
||||||
public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithUsersAndSponsorships_ReturnsCorrectCounts(
|
public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithUsersAndSponsorships_ReturnsCorrectCounts(
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@@ -356,7 +109,7 @@ public class OrganizationRepositoryTests
|
|||||||
Assert.Equal(4, result.Total); // Total occupied seats
|
Assert.Equal(4, result.Total); // Total occupied seats
|
||||||
}
|
}
|
||||||
|
|
||||||
[DatabaseTheory, DatabaseData]
|
[Theory, DatabaseData]
|
||||||
public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithNoUsersOrSponsorships_ReturnsZero(
|
public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithNoUsersOrSponsorships_ReturnsZero(
|
||||||
IOrganizationRepository organizationRepository)
|
IOrganizationRepository organizationRepository)
|
||||||
{
|
{
|
||||||
@@ -372,7 +125,7 @@ public class OrganizationRepositoryTests
|
|||||||
Assert.Equal(0, result.Total);
|
Assert.Equal(0, result.Total);
|
||||||
}
|
}
|
||||||
|
|
||||||
[DatabaseTheory, DatabaseData]
|
[Theory, DatabaseData]
|
||||||
public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithOnlyRevokedUsers_ReturnsZero(
|
public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithOnlyRevokedUsers_ReturnsZero(
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@@ -399,7 +152,7 @@ public class OrganizationRepositoryTests
|
|||||||
Assert.Equal(0, result.Total);
|
Assert.Equal(0, result.Total);
|
||||||
}
|
}
|
||||||
|
|
||||||
[DatabaseTheory, DatabaseData]
|
[Theory, DatabaseData]
|
||||||
public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithOnlyExpiredSponsorships_ReturnsZero(
|
public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithOnlyExpiredSponsorships_ReturnsZero(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository)
|
IOrganizationSponsorshipRepository organizationSponsorshipRepository)
|
||||||
@@ -424,7 +177,7 @@ public class OrganizationRepositoryTests
|
|||||||
Assert.Equal(0, result.Total);
|
Assert.Equal(0, result.Total);
|
||||||
}
|
}
|
||||||
|
|
||||||
[DatabaseTheory, DatabaseData]
|
[Theory, DatabaseData]
|
||||||
public async Task IncrementSeatCountAsync_IncrementsSeatCount(IOrganizationRepository organizationRepository)
|
public async Task IncrementSeatCountAsync_IncrementsSeatCount(IOrganizationRepository organizationRepository)
|
||||||
{
|
{
|
||||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||||
@@ -438,7 +191,7 @@ public class OrganizationRepositoryTests
|
|||||||
Assert.Equal(8, result.Seats);
|
Assert.Equal(8, result.Seats);
|
||||||
}
|
}
|
||||||
|
|
||||||
[DatabaseData, DatabaseTheory]
|
[DatabaseData, Theory]
|
||||||
public async Task IncrementSeatCountAsync_GivenOrganizationHasNotChangedSeatCountBefore_WhenUpdatingOrgSeats_ThenSubscriptionUpdateIsSaved(
|
public async Task IncrementSeatCountAsync_GivenOrganizationHasNotChangedSeatCountBefore_WhenUpdatingOrgSeats_ThenSubscriptionUpdateIsSaved(
|
||||||
IOrganizationRepository sutRepository)
|
IOrganizationRepository sutRepository)
|
||||||
{
|
{
|
||||||
@@ -462,7 +215,7 @@ public class OrganizationRepositoryTests
|
|||||||
await sutRepository.DeleteAsync(organization);
|
await sutRepository.DeleteAsync(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
[DatabaseData, DatabaseTheory]
|
[DatabaseData, Theory]
|
||||||
public async Task IncrementSeatCountAsync_GivenOrganizationHasChangedSeatCountBeforeAndRecordExists_WhenUpdatingOrgSeats_ThenSubscriptionUpdateIsSaved(
|
public async Task IncrementSeatCountAsync_GivenOrganizationHasChangedSeatCountBeforeAndRecordExists_WhenUpdatingOrgSeats_ThenSubscriptionUpdateIsSaved(
|
||||||
IOrganizationRepository sutRepository)
|
IOrganizationRepository sutRepository)
|
||||||
{
|
{
|
||||||
@@ -487,7 +240,7 @@ public class OrganizationRepositoryTests
|
|||||||
await sutRepository.DeleteAsync(organization);
|
await sutRepository.DeleteAsync(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
[DatabaseData, DatabaseTheory]
|
[DatabaseData, Theory]
|
||||||
public async Task GetOrganizationsForSubscriptionSyncAsync_GivenOrganizationHasChangedSeatCount_WhenGettingOrgsToUpdate_ThenReturnsOrgSubscriptionUpdate(
|
public async Task GetOrganizationsForSubscriptionSyncAsync_GivenOrganizationHasChangedSeatCount_WhenGettingOrgsToUpdate_ThenReturnsOrgSubscriptionUpdate(
|
||||||
IOrganizationRepository sutRepository)
|
IOrganizationRepository sutRepository)
|
||||||
{
|
{
|
||||||
@@ -510,7 +263,7 @@ public class OrganizationRepositoryTests
|
|||||||
await sutRepository.DeleteAsync(organization);
|
await sutRepository.DeleteAsync(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
[DatabaseData, DatabaseTheory]
|
[DatabaseData, Theory]
|
||||||
public async Task UpdateSuccessfulOrganizationSyncStatusAsync_GivenOrganizationHasChangedSeatCount_WhenUpdatingStatus_ThenSuccessfullyUpdatesOrgSoItDoesntSync(
|
public async Task UpdateSuccessfulOrganizationSyncStatusAsync_GivenOrganizationHasChangedSeatCount_WhenUpdatingStatus_ThenSuccessfullyUpdatesOrgSoItDoesntSync(
|
||||||
IOrganizationRepository sutRepository)
|
IOrganizationRepository sutRepository)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.OrganizationUserRepository;
|
||||||
|
|
||||||
|
public class GetManyByOrganizationWithClaimedDomainsAsyncTests
|
||||||
|
{
|
||||||
|
[Theory, DatabaseData]
|
||||||
|
public async Task WithVerifiedDomain_WithOneMatchingEmailDomain_ReturnsSingle(
|
||||||
|
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 user3 = await userRepository.CreateAsync(new User
|
||||||
|
{
|
||||||
|
Name = "Test User 3",
|
||||||
|
Email = $"test+{id}@{domainName}.example.com", // Different domain
|
||||||
|
ApiKey = "TEST",
|
||||||
|
SecurityStamp = "stamp",
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = 1,
|
||||||
|
KdfMemory = 2,
|
||||||
|
KdfParallelism = 3
|
||||||
|
});
|
||||||
|
|
||||||
|
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||||
|
|
||||||
|
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.CreateConfirmedTestOrganizationUserAsync(organization, user1);
|
||||||
|
await organizationUserRepository.CreateConfirmedTestOrganizationUserAsync(organization, user2);
|
||||||
|
await organizationUserRepository.CreateConfirmedTestOrganizationUserAsync(organization, user3);
|
||||||
|
|
||||||
|
var result = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organization.Id);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal(orgUser1.Id, result.Single().Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, DatabaseData]
|
||||||
|
public async Task WithNoVerifiedDomain_ReturnsEmpty(
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IOrganizationDomainRepository organizationDomainRepository)
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
var domainName = $"{id}.example.com";
|
||||||
|
|
||||||
|
var user = await userRepository.CreateAsync(new User
|
||||||
|
{
|
||||||
|
Name = "Test User",
|
||||||
|
Email = $"test+{id}@{domainName}",
|
||||||
|
ApiKey = "TEST",
|
||||||
|
SecurityStamp = "stamp",
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = 1,
|
||||||
|
KdfMemory = 2,
|
||||||
|
KdfParallelism = 3
|
||||||
|
});
|
||||||
|
|
||||||
|
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||||
|
|
||||||
|
// Create domain but do NOT verify it
|
||||||
|
var organizationDomain = new OrganizationDomain
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
DomainName = domainName,
|
||||||
|
Txt = "btw+12345",
|
||||||
|
};
|
||||||
|
organizationDomain.SetNextRunDate(12);
|
||||||
|
// Note: NOT calling SetVerifiedDate()
|
||||||
|
await organizationDomainRepository.CreateAsync(organizationDomain);
|
||||||
|
|
||||||
|
await organizationUserRepository.CreateConfirmedTestOrganizationUserAsync(organization, user);
|
||||||
|
|
||||||
|
var result = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organization.Id);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests an edge case where some invited users are created linked to a UserId.
|
||||||
|
/// This is defective behavior, but will take longer to fix - for now, we are defensive and expressly
|
||||||
|
/// exclude such users from the results without relying on the inner join only.
|
||||||
|
/// Invited-revoked users linked to a UserId remain intentionally unhandled for now as they have not caused
|
||||||
|
/// any issues to date and we want to minimize edge cases.
|
||||||
|
/// We will fix the underlying issue going forward: https://bitwarden.atlassian.net/browse/PM-22405
|
||||||
|
/// </summary>
|
||||||
|
[Theory, DatabaseData]
|
||||||
|
public async Task WithVerifiedDomain_ExcludesInvitedUsers(
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IOrganizationDomainRepository organizationDomainRepository)
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
var domainName = $"{id}.example.com";
|
||||||
|
|
||||||
|
var invitedUser = await userRepository.CreateAsync(new User
|
||||||
|
{
|
||||||
|
Name = "Invited User",
|
||||||
|
Email = $"invited+{id}@{domainName}",
|
||||||
|
ApiKey = "TEST",
|
||||||
|
SecurityStamp = "stamp",
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = 1,
|
||||||
|
KdfMemory = 2,
|
||||||
|
KdfParallelism = 3
|
||||||
|
});
|
||||||
|
|
||||||
|
var confirmedUser = await userRepository.CreateAsync(new User
|
||||||
|
{
|
||||||
|
Name = "Confirmed User",
|
||||||
|
Email = $"confirmed+{id}@{domainName}",
|
||||||
|
ApiKey = "TEST",
|
||||||
|
SecurityStamp = "stamp",
|
||||||
|
Kdf = KdfType.PBKDF2_SHA256,
|
||||||
|
KdfIterations = 1,
|
||||||
|
KdfMemory = 2,
|
||||||
|
KdfParallelism = 3
|
||||||
|
});
|
||||||
|
|
||||||
|
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||||
|
|
||||||
|
var organizationDomain = new OrganizationDomain
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
DomainName = domainName,
|
||||||
|
Txt = "btw+12345",
|
||||||
|
};
|
||||||
|
organizationDomain.SetVerifiedDate();
|
||||||
|
organizationDomain.SetNextRunDate(12);
|
||||||
|
organizationDomain.SetJobRunCount();
|
||||||
|
await organizationDomainRepository.CreateAsync(organizationDomain);
|
||||||
|
|
||||||
|
// Create invited user with UserId set (edge case - should be excluded even with UserId linked)
|
||||||
|
var invitedOrgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
UserId = invitedUser.Id, // Edge case: invited user with UserId set
|
||||||
|
Email = invitedUser.Email,
|
||||||
|
Status = OrganizationUserStatusType.Invited,
|
||||||
|
Type = OrganizationUserType.User
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create confirmed user linked by UserId only (no Email field set)
|
||||||
|
var confirmedOrgUser = await organizationUserRepository.CreateConfirmedTestOrganizationUserAsync(organization, confirmedUser);
|
||||||
|
|
||||||
|
var result = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organization.Id);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
var claimedUser = Assert.Single(result);
|
||||||
|
Assert.Equal(confirmedOrgUser.Id, claimedUser.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -599,136 +599,6 @@ public class OrganizationUserRepositoryTests
|
|||||||
Assert.Null(orgWithoutSsoDetails.SsoConfig);
|
Assert.Null(orgWithoutSsoDetails.SsoConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
[DatabaseTheory, DatabaseData]
|
|
||||||
public async Task GetManyByOrganizationWithClaimedDomainsAsync_WithVerifiedDomain_WithOneMatchingEmailDomain_ReturnsSingle(
|
|
||||||
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 user3 = await userRepository.CreateAsync(new User
|
|
||||||
{
|
|
||||||
Name = "Test User 2",
|
|
||||||
Email = $"test+{id}@{domainName}.example.com", // 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",
|
|
||||||
UsePolicies = false,
|
|
||||||
UseSso = false,
|
|
||||||
UseKeyConnector = false,
|
|
||||||
UseScim = false,
|
|
||||||
UseGroups = false,
|
|
||||||
UseDirectory = false,
|
|
||||||
UseEvents = false,
|
|
||||||
UseTotp = false,
|
|
||||||
Use2fa = false,
|
|
||||||
UseApi = false,
|
|
||||||
UseResetPassword = false,
|
|
||||||
UseSecretsManager = false,
|
|
||||||
SelfHost = false,
|
|
||||||
UsersGetPremium = false,
|
|
||||||
UseCustomPermissions = false,
|
|
||||||
Enabled = true,
|
|
||||||
UsePasswordManager = false,
|
|
||||||
LimitCollectionCreation = false,
|
|
||||||
LimitCollectionDeletion = false,
|
|
||||||
LimitItemDeletion = false,
|
|
||||||
AllowAdminAccessToAllCollectionItems = false,
|
|
||||||
UseRiskInsights = false,
|
|
||||||
UseAdminSponsoredFamilies = false,
|
|
||||||
UsePhishingBlocker = false,
|
|
||||||
UseDisableSmAdsForUsers = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
Id = CoreHelpers.GenerateComb(),
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
UserId = user1.Id,
|
|
||||||
Status = OrganizationUserStatusType.Confirmed,
|
|
||||||
Type = OrganizationUserType.Owner,
|
|
||||||
ResetPasswordKey = "resetpasswordkey1",
|
|
||||||
AccessSecretsManager = false
|
|
||||||
});
|
|
||||||
|
|
||||||
await organizationUserRepository.CreateAsync(new OrganizationUser
|
|
||||||
{
|
|
||||||
Id = CoreHelpers.GenerateComb(),
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
UserId = user2.Id,
|
|
||||||
Status = OrganizationUserStatusType.Confirmed,
|
|
||||||
Type = OrganizationUserType.User,
|
|
||||||
ResetPasswordKey = "resetpasswordkey1",
|
|
||||||
AccessSecretsManager = false
|
|
||||||
});
|
|
||||||
|
|
||||||
await organizationUserRepository.CreateAsync(new OrganizationUser
|
|
||||||
{
|
|
||||||
Id = CoreHelpers.GenerateComb(),
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
UserId = user3.Id,
|
|
||||||
Status = OrganizationUserStatusType.Confirmed,
|
|
||||||
Type = OrganizationUserType.User,
|
|
||||||
ResetPasswordKey = "resetpasswordkey1",
|
|
||||||
AccessSecretsManager = false
|
|
||||||
});
|
|
||||||
|
|
||||||
var responseModel = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organization.Id);
|
|
||||||
|
|
||||||
Assert.NotNull(responseModel);
|
|
||||||
Assert.Single(responseModel);
|
|
||||||
Assert.Equal(orgUser1.Id, responseModel.Single().Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
[DatabaseTheory, DatabaseData]
|
[DatabaseTheory, DatabaseData]
|
||||||
public async Task CreateManyAsync_NoId_Works(IOrganizationRepository organizationRepository,
|
public async Task CreateManyAsync_NoId_Works(IOrganizationRepository organizationRepository,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
@@ -1237,70 +1107,6 @@ public class OrganizationUserRepositoryTests
|
|||||||
Assert.DoesNotContain(user1Result.Collections, c => c.Id == defaultUserCollection.Id);
|
Assert.DoesNotContain(user1Result.Collections, c => c.Id == defaultUserCollection.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[DatabaseTheory, DatabaseData]
|
|
||||||
public async Task GetManyByOrganizationWithClaimedDomainsAsync_WithNoVerifiedDomain_ReturnsEmpty(
|
|
||||||
IUserRepository userRepository,
|
|
||||||
IOrganizationRepository organizationRepository,
|
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
|
||||||
IOrganizationDomainRepository organizationDomainRepository)
|
|
||||||
{
|
|
||||||
var id = Guid.NewGuid();
|
|
||||||
var domainName = $"{id}.example.com";
|
|
||||||
var requestTime = DateTime.UtcNow;
|
|
||||||
|
|
||||||
var user1 = await userRepository.CreateAsync(new User
|
|
||||||
{
|
|
||||||
Id = CoreHelpers.GenerateComb(),
|
|
||||||
Name = "Test User 1",
|
|
||||||
Email = $"test+{id}@{domainName}",
|
|
||||||
ApiKey = "TEST",
|
|
||||||
SecurityStamp = "stamp",
|
|
||||||
CreationDate = requestTime,
|
|
||||||
RevisionDate = requestTime,
|
|
||||||
AccountRevisionDate = requestTime
|
|
||||||
});
|
|
||||||
|
|
||||||
var organization = await organizationRepository.CreateAsync(new Organization
|
|
||||||
{
|
|
||||||
Id = CoreHelpers.GenerateComb(),
|
|
||||||
Name = $"Test Org {id}",
|
|
||||||
BillingEmail = user1.Email,
|
|
||||||
Plan = "Test",
|
|
||||||
Enabled = true,
|
|
||||||
CreationDate = requestTime,
|
|
||||||
RevisionDate = requestTime
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create domain but do NOT verify it
|
|
||||||
var organizationDomain = new OrganizationDomain
|
|
||||||
{
|
|
||||||
Id = CoreHelpers.GenerateComb(),
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
DomainName = domainName,
|
|
||||||
Txt = "btw+12345",
|
|
||||||
CreationDate = requestTime
|
|
||||||
};
|
|
||||||
organizationDomain.SetNextRunDate(12);
|
|
||||||
// Note: NOT calling SetVerifiedDate()
|
|
||||||
await organizationDomainRepository.CreateAsync(organizationDomain);
|
|
||||||
|
|
||||||
await organizationUserRepository.CreateAsync(new OrganizationUser
|
|
||||||
{
|
|
||||||
Id = CoreHelpers.GenerateComb(),
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
UserId = user1.Id,
|
|
||||||
Status = OrganizationUserStatusType.Confirmed,
|
|
||||||
Type = OrganizationUserType.Owner,
|
|
||||||
CreationDate = requestTime,
|
|
||||||
RevisionDate = requestTime
|
|
||||||
});
|
|
||||||
|
|
||||||
var responseModel = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organization.Id);
|
|
||||||
|
|
||||||
Assert.NotNull(responseModel);
|
|
||||||
Assert.Empty(responseModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
[DatabaseTheory, DatabaseData]
|
[DatabaseTheory, DatabaseData]
|
||||||
public async Task DeleteAsync_WithNullEmail_DoesNotSetDefaultUserCollectionEmail(IUserRepository userRepository,
|
public async Task DeleteAsync_WithNullEmail_DoesNotSetDefaultUserCollectionEmail(IUserRepository userRepository,
|
||||||
ICollectionRepository collectionRepository,
|
ICollectionRepository collectionRepository,
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Organization_ReadByClaimedUserEmailDomain]
|
||||||
|
@UserId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
WITH CTE_User AS (
|
||||||
|
SELECT
|
||||||
|
U.[Id],
|
||||||
|
SUBSTRING(U.Email, CHARINDEX('@', U.Email) + 1, LEN(U.Email)) AS EmailDomain
|
||||||
|
FROM dbo.[UserView] U
|
||||||
|
WHERE U.[Id] = @UserId
|
||||||
|
)
|
||||||
|
SELECT O.*
|
||||||
|
FROM CTE_User CU
|
||||||
|
INNER JOIN dbo.[OrganizationUserView] OU ON CU.[Id] = OU.[UserId]
|
||||||
|
INNER JOIN dbo.[OrganizationView] O ON OU.[OrganizationId] = O.[Id]
|
||||||
|
INNER JOIN dbo.[OrganizationDomainView] OD ON OU.[OrganizationId] = OD.[OrganizationId]
|
||||||
|
WHERE OD.[VerifiedDate] IS NOT NULL
|
||||||
|
AND CU.EmailDomain = OD.[DomainName]
|
||||||
|
AND O.[Enabled] = 1
|
||||||
|
AND OU.[Status] != 0 -- Exclude invited users
|
||||||
|
END
|
||||||
|
GO
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2]
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
WITH OrgUsers AS (
|
||||||
|
SELECT *
|
||||||
|
FROM [dbo].[OrganizationUserView]
|
||||||
|
WHERE [OrganizationId] = @OrganizationId
|
||||||
|
AND [Status] != 0 -- Exclude invited users
|
||||||
|
),
|
||||||
|
UserDomains AS (
|
||||||
|
SELECT U.[Id], U.[EmailDomain]
|
||||||
|
FROM [dbo].[UserEmailDomainView] U
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM [dbo].[OrganizationDomainView] OD
|
||||||
|
WHERE OD.[OrganizationId] = @OrganizationId
|
||||||
|
AND OD.[VerifiedDate] IS NOT NULL
|
||||||
|
AND OD.[DomainName] = U.[EmailDomain]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
SELECT OU.*
|
||||||
|
FROM OrgUsers OU
|
||||||
|
JOIN UserDomains UD ON OU.[UserId] = UD.[Id]
|
||||||
|
OPTION (RECOMPILE);
|
||||||
|
END
|
||||||
|
GO
|
||||||
Reference in New Issue
Block a user