mirror of
https://github.com/bitwarden/server
synced 2026-01-29 15:53:36 +00:00
Merge branch 'main' into billing/pm-29595/user-that-upgraded-from-premium-reverts-an-organization-upgrade-during-the-trial-period
This commit is contained in:
@@ -23,11 +23,9 @@
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
},
|
||||
"Console": {
|
||||
"IncludeScopes": true,
|
||||
|
||||
@@ -20,11 +20,9 @@
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
},
|
||||
"Console": {
|
||||
"IncludeScopes": true,
|
||||
|
||||
@@ -23,11 +23,9 @@
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
},
|
||||
"Console": {
|
||||
"IncludeScopes": true,
|
||||
|
||||
@@ -292,17 +292,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);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -8,7 +9,7 @@ namespace Bit.Core.Utilities;
|
||||
public static class LoggerFactoryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="hostBuilder"></param>
|
||||
/// <returns></returns>
|
||||
@@ -21,10 +22,12 @@ public static class LoggerFactoryExtensions
|
||||
return;
|
||||
}
|
||||
|
||||
IConfiguration loggingConfiguration;
|
||||
|
||||
// If they have begun using the new settings location, use that
|
||||
if (!string.IsNullOrEmpty(context.Configuration["Logging:PathFormat"]))
|
||||
{
|
||||
logging.AddFile(context.Configuration.GetSection("Logging"));
|
||||
loggingConfiguration = context.Configuration.GetSection("Logging");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -40,28 +43,35 @@ public static class LoggerFactoryExtensions
|
||||
var projectName = loggingOptions.ProjectName
|
||||
?? context.HostingEnvironment.ApplicationName;
|
||||
|
||||
string pathFormat;
|
||||
|
||||
if (loggingOptions.LogRollBySizeLimit.HasValue)
|
||||
{
|
||||
var pathFormat = loggingOptions.LogDirectoryByProject
|
||||
pathFormat = loggingOptions.LogDirectoryByProject
|
||||
? Path.Combine(loggingOptions.LogDirectory, projectName, "log.txt")
|
||||
: Path.Combine(loggingOptions.LogDirectory, $"{projectName.ToLowerInvariant()}.log");
|
||||
|
||||
logging.AddFile(
|
||||
pathFormat: pathFormat,
|
||||
fileSizeLimitBytes: loggingOptions.LogRollBySizeLimit.Value
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
var pathFormat = loggingOptions.LogDirectoryByProject
|
||||
pathFormat = loggingOptions.LogDirectoryByProject
|
||||
? Path.Combine(loggingOptions.LogDirectory, projectName, "{Date}.txt")
|
||||
: Path.Combine(loggingOptions.LogDirectory, $"{projectName.ToLowerInvariant()}_{{Date}}.log");
|
||||
|
||||
logging.AddFile(
|
||||
pathFormat: pathFormat
|
||||
);
|
||||
}
|
||||
|
||||
// We want to rely on Serilog using the configuration section to have customization of the log levels
|
||||
// so we make a custom configuration source for them based on the legacy values and allow overrides from
|
||||
// the new location.
|
||||
loggingConfiguration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
{"PathFormat", pathFormat},
|
||||
{"FileSizeLimitBytes", loggingOptions.LogRollBySizeLimit?.ToString(CultureInfo.InvariantCulture)}
|
||||
})
|
||||
.AddConfiguration(context.Configuration.GetSection("Logging"))
|
||||
.Build();
|
||||
}
|
||||
|
||||
logging.AddFile(loggingConfiguration);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,9 @@
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
},
|
||||
"Console": {
|
||||
"IncludeScopes": true,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
},
|
||||
"Console": {
|
||||
"IncludeScopes": true,
|
||||
|
||||
@@ -17,11 +17,9 @@
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
},
|
||||
"Console": {
|
||||
"IncludeScopes": true,
|
||||
|
||||
@@ -20,11 +20,9 @@
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
},
|
||||
"Console": {
|
||||
"IncludeScopes": true,
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -17,11 +17,9 @@
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning"
|
||||
},
|
||||
"Console": {
|
||||
"IncludeScopes": true,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,6 +12,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;
|
||||
@@ -657,6 +658,8 @@ public class SubscriptionUpdatedHandlerTests
|
||||
var plan = new Enterprise2023Plan(true);
|
||||
_pricingClient.GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(plan);
|
||||
_pricingClient.ListPlans()
|
||||
.Returns(MockPlans.Plans);
|
||||
|
||||
var parsedEvent = new Event
|
||||
{
|
||||
@@ -696,6 +699,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))]
|
||||
|
||||
@@ -74,8 +74,7 @@ public class LoggerFactoryExtensionsTests
|
||||
|
||||
logger.LogWarning("This is a test");
|
||||
|
||||
// Writing to the file is buffered, give it a little time to flush
|
||||
await Task.Delay(5);
|
||||
await provider.DisposeAsync();
|
||||
|
||||
var logFile = Assert.Single(tempDir.EnumerateFiles("Logs/*.log"));
|
||||
|
||||
@@ -90,13 +89,67 @@ public class LoggerFactoryExtensionsTests
|
||||
logFileContents
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddSerilogFileLogging_LegacyConfig_WithLevelCustomization_InfoLogs_DoNotFillUpFile()
|
||||
{
|
||||
await AssertSmallFileAsync((tempDir, config) =>
|
||||
{
|
||||
config["GlobalSettings:LogDirectory"] = tempDir;
|
||||
config["Logging:LogLevel:Microsoft.AspNetCore"] = "Warning";
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddSerilogFileLogging_NewConfig_WithLevelCustomization_InfoLogs_DoNotFillUpFile()
|
||||
{
|
||||
await AssertSmallFileAsync((tempDir, config) =>
|
||||
{
|
||||
config["Logging:PathFormat"] = Path.Combine(tempDir, "log.txt");
|
||||
config["Logging:LogLevel:Microsoft.AspNetCore"] = "Warning";
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task AssertSmallFileAsync(Action<string, Dictionary<string, string?>> configure)
|
||||
{
|
||||
using var tempDir = new TempDirectory();
|
||||
var config = new Dictionary<string, string?>();
|
||||
|
||||
configure(tempDir.Directory, config);
|
||||
|
||||
var provider = GetServiceProvider(config, "Production");
|
||||
|
||||
var loggerFactory = provider.GetRequiredService<ILoggerFactory>();
|
||||
var microsoftLogger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Testing");
|
||||
|
||||
for (var i = 0; i < 100; i++)
|
||||
{
|
||||
microsoftLogger.LogInformation("Tons of useless information");
|
||||
}
|
||||
|
||||
var otherLogger = loggerFactory.CreateLogger("Bitwarden");
|
||||
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
otherLogger.LogInformation("Mildly more useful information but not as frequent.");
|
||||
}
|
||||
|
||||
await provider.DisposeAsync();
|
||||
|
||||
var logFiles = Directory.EnumerateFiles(tempDir.Directory, "*.txt", SearchOption.AllDirectories);
|
||||
var logFile = Assert.Single(logFiles);
|
||||
|
||||
using var fr = File.OpenRead(logFile);
|
||||
Assert.InRange(fr.Length, 0, 1024);
|
||||
}
|
||||
|
||||
private static IEnumerable<ILoggerProvider> GetProviders(Dictionary<string, string?> initialData, string environment = "Production")
|
||||
{
|
||||
var provider = GetServiceProvider(initialData, environment);
|
||||
return provider.GetServices<ILoggerProvider>();
|
||||
}
|
||||
|
||||
private static IServiceProvider GetServiceProvider(Dictionary<string, string?> initialData, string environment)
|
||||
private static ServiceProvider GetServiceProvider(Dictionary<string, string?> initialData, string environment)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(initialData)
|
||||
|
||||
@@ -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