mirror of
https://github.com/bitwarden/server
synced 2026-01-31 00:33:17 +00:00
Merge branch 'main' into ac/pm-30610/fix-formatting
This commit is contained in:
@@ -275,17 +275,24 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
.PreviousAttributes
|
||||
.ToObject<Subscription>() as Subscription;
|
||||
|
||||
// Get all plan IDs that include Secrets Manager support to check if the organization has secret manager in the
|
||||
// previous and/or current subscriptions.
|
||||
var planIdsOfPlansWithSecretManager = (await _pricingClient.ListPlans())
|
||||
.Where(orgPlan => orgPlan.SupportsSecretsManager && orgPlan.SecretsManager.StripeSeatPlanId != null)
|
||||
.Select(orgPlan => orgPlan.SecretsManager.StripeSeatPlanId)
|
||||
.ToHashSet();
|
||||
|
||||
// This being false doesn't necessarily mean that the organization doesn't subscribe to Secrets Manager.
|
||||
// If there are changes to any subscription item, Stripe sends every item in the subscription, both
|
||||
// changed and unchanged.
|
||||
var previousSubscriptionHasSecretsManager =
|
||||
previousSubscription?.Items is not null &&
|
||||
previousSubscription.Items.Any(
|
||||
previousSubscriptionItem => previousSubscriptionItem.Plan.Id == plan.SecretsManager.StripeSeatPlanId);
|
||||
previousSubscriptionItem => planIdsOfPlansWithSecretManager.Contains(previousSubscriptionItem.Plan.Id));
|
||||
|
||||
var currentSubscriptionHasSecretsManager =
|
||||
subscription.Items.Any(
|
||||
currentSubscriptionItem => currentSubscriptionItem.Plan.Id == plan.SecretsManager.StripeSeatPlanId);
|
||||
currentSubscriptionItem => planIdsOfPlansWithSecretManager.Contains(currentSubscriptionItem.Plan.Id));
|
||||
|
||||
if (!previousSubscriptionHasSecretsManager || currentSubscriptionHasSecretsManager)
|
||||
{
|
||||
|
||||
@@ -21,7 +21,9 @@ public interface IOrganizationRepository : IRepository<Organization, Guid>
|
||||
Task<IEnumerable<string>> GetOwnerEmailAddressesById(Guid organizationId);
|
||||
|
||||
/// <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>
|
||||
Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId);
|
||||
|
||||
|
||||
@@ -6,12 +6,52 @@
|
||||
<mj-body css-class="border-fix">
|
||||
<!-- Blue Header Section -->
|
||||
<mj-wrapper css-class="border-fix" padding="20px 20px 10px 20px">
|
||||
<mj-bw-hero
|
||||
img-src="https://assets.bitwarden.com/email/v1/spot-family-homes.png"
|
||||
title="You can now share passwords with members of <b>{{OrganizationName}}!</b>"
|
||||
button-text="<b>Log in</b>"
|
||||
button-url="{{WebVaultUrl}}"
|
||||
/>
|
||||
<mj-section
|
||||
full-width="full-width"
|
||||
background-color="#175ddc"
|
||||
border-radius="4px 4px 0px 0px"
|
||||
>
|
||||
<mj-column width="70%">
|
||||
<mj-image
|
||||
align="left"
|
||||
src="https://bitwarden.com/images/logo-horizontal-white.png"
|
||||
width="150px"
|
||||
height="30px"
|
||||
/>
|
||||
|
||||
<mj-text color="#fff" padding-top="0" padding-bottom="0">
|
||||
<h1 style="font-weight: normal; font-size: 24px; line-height: 32px">
|
||||
You can now share passwords with members of <b>{{OrganizationName}}!</b>
|
||||
</h1>
|
||||
</mj-text>
|
||||
|
||||
<mj-button
|
||||
href="{{WebVaultUrl}}"
|
||||
background-color="#ffffff"
|
||||
color="#1A41AC"
|
||||
border-radius="20px"
|
||||
align="left"
|
||||
inner-padding="12px 24px"
|
||||
font-size="16px"
|
||||
font-family="Helvetica Neue, Helvetica, Arial, sans-serif"
|
||||
font-weight="700"
|
||||
line-height="24px"
|
||||
>
|
||||
Log in
|
||||
</mj-button>
|
||||
|
||||
</mj-column>
|
||||
|
||||
<mj-column width="30%" vertical-align="bottom">
|
||||
<mj-image
|
||||
src="https://assets.bitwarden.com/email/v1/spot-family-homes.png"
|
||||
alt=""
|
||||
width="155px"
|
||||
padding="0px"
|
||||
css-class="mj-bw-hero-responsive-img"
|
||||
/>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-wrapper>
|
||||
|
||||
<!-- Main Content -->
|
||||
|
||||
@@ -325,7 +325,8 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
||||
where ou.UserId == userWithDomain.UserId &&
|
||||
od.DomainName == userWithDomain.EmailDomain &&
|
||||
od.VerifiedDate != null &&
|
||||
o.Enabled == true
|
||||
o.Enabled == true &&
|
||||
ou.Status != OrganizationUserStatusType.Invited
|
||||
select o;
|
||||
|
||||
return await query.ToArrayAsync();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
||||
|
||||
@@ -16,6 +17,7 @@ public class OrganizationUserReadByClaimedOrganizationDomainsQuery : IQuery<Orga
|
||||
var query = from ou in dbContext.OrganizationUsers
|
||||
join u in dbContext.Users on ou.UserId equals u.Id
|
||||
where ou.OrganizationId == _organizationId
|
||||
&& ou.Status != OrganizationUserStatusType.Invited
|
||||
&& dbContext.OrganizationDomains
|
||||
.Any(od => od.OrganizationId == _organizationId &&
|
||||
od.VerifiedDate != null &&
|
||||
|
||||
@@ -8,13 +8,14 @@ BEGIN
|
||||
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
|
||||
FROM [dbo].[OrganizationDomainView] OD
|
||||
WHERE OD.[OrganizationId] = @OrganizationId
|
||||
AND OD.[VerifiedDate] IS NOT NULL
|
||||
AND OD.[DomainName] = U.[EmailDomain]
|
||||
|
||||
@@ -6,7 +6,7 @@ BEGIN
|
||||
|
||||
WITH CTE_User AS (
|
||||
SELECT
|
||||
U.*,
|
||||
U.[Id],
|
||||
SUBSTRING(U.Email, CHARINDEX('@', U.Email) + 1, LEN(U.Email)) AS EmailDomain
|
||||
FROM dbo.[UserView] U
|
||||
WHERE U.[Id] = @UserId
|
||||
@@ -19,4 +19,5 @@ BEGIN
|
||||
WHERE OD.[VerifiedDate] IS NOT NULL
|
||||
AND CU.EmailDomain = OD.[DomainName]
|
||||
AND O.[Enabled] = 1
|
||||
AND OU.[Status] != 0 -- Exclude invited users
|
||||
END
|
||||
|
||||
@@ -11,6 +11,7 @@ using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Core.Test.Billing.Mocks.Plans;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@@ -654,6 +655,8 @@ public class SubscriptionUpdatedHandlerTests
|
||||
var plan = new Enterprise2023Plan(true);
|
||||
_pricingClient.GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(plan);
|
||||
_pricingClient.ListPlans()
|
||||
.Returns(MockPlans.Plans);
|
||||
|
||||
var parsedEvent = new Event
|
||||
{
|
||||
@@ -693,6 +696,92 @@ public class SubscriptionUpdatedHandlerTests
|
||||
await _stripeFacade.Received(1).DeleteCustomerDiscount(subscription.CustomerId);
|
||||
await _stripeFacade.Received(1).DeleteSubscriptionDiscount(subscription.Id);
|
||||
}
|
||||
[Fact]
|
||||
public async Task
|
||||
HandleAsync_WhenUpgradingPlan_AndPreviousPlanHasSecretsManagerTrial_AndCurrentPlanHasSecretsManagerTrial_DoesNotRemovePasswordManagerCoupon()
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_123",
|
||||
Status = StripeSubscriptionStatus.Active,
|
||||
CustomerId = "cus_123",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem
|
||||
{
|
||||
CurrentPeriodEnd = DateTime.UtcNow.AddDays(10),
|
||||
Plan = new Plan { Id = "2023-enterprise-org-seat-annually" }
|
||||
},
|
||||
new SubscriptionItem
|
||||
{
|
||||
CurrentPeriodEnd = DateTime.UtcNow.AddDays(10),
|
||||
Plan = new Plan { Id = "secrets-manager-enterprise-seat-annually" }
|
||||
}
|
||||
]
|
||||
},
|
||||
Customer = new Customer
|
||||
{
|
||||
Balance = 0,
|
||||
Discount = new Discount { Coupon = new Coupon { Id = "sm-standalone" } }
|
||||
},
|
||||
Discounts = [new Discount { Coupon = new Coupon { Id = "sm-standalone" } }],
|
||||
Metadata = new Dictionary<string, string> { { "organizationId", organizationId.ToString() } }
|
||||
};
|
||||
|
||||
// Note: The organization plan is still the previous plan because the subscription is updated before the organization is updated
|
||||
var organization = new Organization { Id = organizationId, PlanType = PlanType.TeamsAnnually2023 };
|
||||
|
||||
var plan = new Teams2023Plan(true);
|
||||
_pricingClient.GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(plan);
|
||||
_pricingClient.ListPlans()
|
||||
.Returns(MockPlans.Plans);
|
||||
|
||||
var parsedEvent = new Event
|
||||
{
|
||||
Data = new EventData
|
||||
{
|
||||
Object = subscription,
|
||||
PreviousAttributes = JObject.FromObject(new
|
||||
{
|
||||
items = new
|
||||
{
|
||||
data = new[]
|
||||
{
|
||||
new { plan = new { id = "secrets-manager-teams-seat-annually" } },
|
||||
}
|
||||
},
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem { Plan = new Stripe.Plan { Id = "secrets-manager-teams-seat-annually" } },
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
|
||||
.Returns(subscription);
|
||||
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(organizationId, null, null));
|
||||
|
||||
_organizationRepository.GetByIdAsync(organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.DidNotReceive().DeleteCustomerDiscount(subscription.CustomerId);
|
||||
await _stripeFacade.DidNotReceive().DeleteSubscriptionDiscount(subscription.Id);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GetNonActiveSubscriptions))]
|
||||
|
||||
@@ -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
|
||||
{
|
||||
[DatabaseTheory, 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]
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetManyByIdsAsync_ExistingOrganizations_ReturnsOrganizations(IOrganizationRepository organizationRepository)
|
||||
{
|
||||
var email = "test@email.com";
|
||||
@@ -287,7 +40,7 @@ public class OrganizationRepositoryTests
|
||||
await organizationRepository.DeleteAsync(organization2);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithUsersAndSponsorships_ReturnsCorrectCounts(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
@@ -356,7 +109,7 @@ public class OrganizationRepositoryTests
|
||||
Assert.Equal(4, result.Total); // Total occupied seats
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithNoUsersOrSponsorships_ReturnsZero(
|
||||
IOrganizationRepository organizationRepository)
|
||||
{
|
||||
@@ -372,7 +125,7 @@ public class OrganizationRepositoryTests
|
||||
Assert.Equal(0, result.Total);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithOnlyRevokedUsers_ReturnsZero(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
@@ -399,7 +152,7 @@ public class OrganizationRepositoryTests
|
||||
Assert.Equal(0, result.Total);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithOnlyExpiredSponsorships_ReturnsZero(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository)
|
||||
@@ -424,7 +177,7 @@ public class OrganizationRepositoryTests
|
||||
Assert.Equal(0, result.Total);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
[Theory, DatabaseData]
|
||||
public async Task IncrementSeatCountAsync_IncrementsSeatCount(IOrganizationRepository organizationRepository)
|
||||
{
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
@@ -438,7 +191,7 @@ public class OrganizationRepositoryTests
|
||||
Assert.Equal(8, result.Seats);
|
||||
}
|
||||
|
||||
[DatabaseData, DatabaseTheory]
|
||||
[DatabaseData, Theory]
|
||||
public async Task IncrementSeatCountAsync_GivenOrganizationHasNotChangedSeatCountBefore_WhenUpdatingOrgSeats_ThenSubscriptionUpdateIsSaved(
|
||||
IOrganizationRepository sutRepository)
|
||||
{
|
||||
@@ -462,7 +215,7 @@ public class OrganizationRepositoryTests
|
||||
await sutRepository.DeleteAsync(organization);
|
||||
}
|
||||
|
||||
[DatabaseData, DatabaseTheory]
|
||||
[DatabaseData, Theory]
|
||||
public async Task IncrementSeatCountAsync_GivenOrganizationHasChangedSeatCountBeforeAndRecordExists_WhenUpdatingOrgSeats_ThenSubscriptionUpdateIsSaved(
|
||||
IOrganizationRepository sutRepository)
|
||||
{
|
||||
@@ -487,7 +240,7 @@ public class OrganizationRepositoryTests
|
||||
await sutRepository.DeleteAsync(organization);
|
||||
}
|
||||
|
||||
[DatabaseData, DatabaseTheory]
|
||||
[DatabaseData, Theory]
|
||||
public async Task GetOrganizationsForSubscriptionSyncAsync_GivenOrganizationHasChangedSeatCount_WhenGettingOrgsToUpdate_ThenReturnsOrgSubscriptionUpdate(
|
||||
IOrganizationRepository sutRepository)
|
||||
{
|
||||
@@ -510,7 +263,7 @@ public class OrganizationRepositoryTests
|
||||
await sutRepository.DeleteAsync(organization);
|
||||
}
|
||||
|
||||
[DatabaseData, DatabaseTheory]
|
||||
[DatabaseData, Theory]
|
||||
public async Task UpdateSuccessfulOrganizationSyncStatusAsync_GivenOrganizationHasChangedSeatCount_WhenUpdatingStatus_ThenSuccessfullyUpdatesOrgSoItDoesntSync(
|
||||
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);
|
||||
}
|
||||
|
||||
[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]
|
||||
public async Task CreateManyAsync_NoId_Works(IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository,
|
||||
@@ -1237,70 +1107,6 @@ public class OrganizationUserRepositoryTests
|
||||
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]
|
||||
public async Task DeleteAsync_WithNullEmail_DoesNotSetDefaultUserCollectionEmail(IUserRepository userRepository,
|
||||
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