From e465f2ed475fac5d9cce45287faa6765044b0be0 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 7 May 2025 08:11:29 -0500 Subject: [PATCH 01/20] remove new device verification flags (#5773) --- src/Core/Constants.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 16b9849451..dfb40260c9 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -197,8 +197,6 @@ public static class FeatureFlagKeys /* Vault Team */ public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge"; public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form"; - public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss"; - public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss"; public const string RestrictProviderAccess = "restrict-provider-access"; public const string SecurityTasks = "security-tasks"; public const string CipherKeyEncryption = "cipher-key-encryption"; From e6c4d78fc1b357d3cf271fcc63d2138759f790cf Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 7 May 2025 13:57:11 -0400 Subject: [PATCH 02/20] chore(feature-flag): [PM-12432] Remove 2fa-authenticator-token feature flag * Completed grouping of feature flags by team. * Completed grouping feature flags by team. * Remove email delay feature flag * Removed feature flag * Fixed reference. * Remove flag after merge. * Removed flag from server. --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index dfb40260c9..90e9e46619 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -115,7 +115,6 @@ public static class FeatureFlagKeys public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; public const string EmailVerification = "email-verification"; public const string DeviceTrustLogging = "pm-8285-device-trust-logging"; - public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token"; public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh"; public const string NewDeviceVerification = "new-device-verification"; public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor"; From 051f200d4b8c307c5285ba1c1d1fca3007727236 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 7 May 2025 17:18:18 -0400 Subject: [PATCH 03/20] [PM-17239] Update Renovate config to configure patch behavior and reassign dependencies (#5775) * Update config to send patch updates to dashboard * Added trailing commas. --- .github/renovate.json5 | 66 ++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 344a326519..ac34903c1b 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -20,7 +20,7 @@ ], commitMessagePrefix: "[deps] BRE:", reviewers: ["team:dept-bre"], - addLabels: ["hold"] + addLabels: ["hold"], }, { groupName: "dockerfile minor", @@ -37,6 +37,16 @@ matchManagers: ["github-actions"], matchUpdateTypes: ["minor"], }, + { + // For any Microsoft.Extensions.* and Microsoft.AspNetCore.* packages, we want to create PRs for patch updates. + // This overrides the default that ignores patch updates for nuget dependencies. + matchPackageNames: [ + "/^Microsoft\\.Extensions\\./", + "/^Microsoft\\.AspNetCore\\./", + ], + matchUpdateTypes: ["patch"], + dependencyDashboardApproval: false, + }, { matchManagers: ["dockerfile", "docker-compose"], commitMessagePrefix: "[deps] BRE:", @@ -59,6 +69,7 @@ "DuoUniversal", "Fido2.AspNet", "Duende.IdentityServer", + "Microsoft.AspNetCore.Authentication.JwtBearer", "Microsoft.Extensions.Identity.Stores", "Otp.NET", "Sustainsys.Saml2.AspNetCore2", @@ -79,8 +90,6 @@ "CsvHelper", "Kralizek.AutoFixture.Extensions.MockHttp", "Microsoft.AspNetCore.Mvc.Testing", - "Microsoft.Extensions.Logging", - "Microsoft.Extensions.Logging.Console", "Newtonsoft.Json", "NSubstitute", "Sentry.Serilog", @@ -100,9 +109,9 @@ reviewers: ["team:team-billing-dev"], }, { - matchPackagePatterns: ["^Microsoft.Extensions.Logging"], - groupName: "Microsoft.Extensions.Logging", - description: "Group Microsoft.Extensions.Logging to exclude them from the dotnet monorepo preset", + matchPackageNames: ["/^Microsoft\\.EntityFrameworkCore\\./", "/^dotnet-ef/"], + groupName: "EntityFrameworkCore", + description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset", }, { matchPackageNames: [ @@ -117,9 +126,6 @@ "Microsoft.EntityFrameworkCore.Relational", "Microsoft.EntityFrameworkCore.Sqlite", "Microsoft.EntityFrameworkCore.SqlServer", - "Microsoft.Extensions.Caching.Cosmos", - "Microsoft.Extensions.Caching.SqlServer", - "Microsoft.Extensions.Caching.StackExchangeRedis", "Npgsql.EntityFrameworkCore.PostgreSQL", "Pomelo.EntityFrameworkCore.MySql", ], @@ -142,56 +148,40 @@ "Azure.Messaging.ServiceBus", "Azure.Storage.Blobs", "Azure.Storage.Queues", - "Microsoft.AspNetCore.Authentication.JwtBearer", + "LaunchDarkly.ServerSdk", "Microsoft.AspNetCore.Http", + "Microsoft.AspNetCore.SignalR.Protocols.MessagePack", + "Microsoft.AspNetCore.SignalR.StackExchangeRedis", + "Microsoft.Extensions.Configuration.EnvironmentVariables", + "Microsoft.Extensions.Configuration.UserSecrets", + "Microsoft.Extensions.Configuration", + "Microsoft.Extensions.DependencyInjection.Abstractions", + "Microsoft.Extensions.DependencyInjection", + "Microsoft.Extensions.Logging", + "Microsoft.Extensions.Logging.Console", + "Microsoft.Extensions.Caching.Cosmos", + "Microsoft.Extensions.Caching.SqlServer", + "Microsoft.Extensions.Caching.StackExchangeRedis", "Quartz", ], description: "Platform owned dependencies", commitMessagePrefix: "[deps] Platform:", reviewers: ["team:team-platform-dev"], }, - { - matchPackagePatterns: ["EntityFrameworkCore", "^dotnet-ef"], - groupName: "EntityFrameworkCore", - description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset", - }, { matchPackageNames: [ "AutoMapper.Extensions.Microsoft.DependencyInjection", "AWSSDK.SimpleEmail", "AWSSDK.SQS", "Handlebars.Net", - "LaunchDarkly.ServerSdk", "MailKit", - "Microsoft.AspNetCore.SignalR.Protocols.MessagePack", - "Microsoft.AspNetCore.SignalR.StackExchangeRedis", "Microsoft.Azure.NotificationHubs", - "Microsoft.Extensions.Configuration.EnvironmentVariables", - "Microsoft.Extensions.Configuration.UserSecrets", - "Microsoft.Extensions.Configuration", - "Microsoft.Extensions.DependencyInjection.Abstractions", - "Microsoft.Extensions.DependencyInjection", "SendGrid", ], description: "Tools owned dependencies", commitMessagePrefix: "[deps] Tools:", reviewers: ["team:team-tools-dev"], }, - { - matchPackagePatterns: ["^Microsoft.AspNetCore.SignalR"], - groupName: "SignalR", - description: "Group SignalR to exclude them from the dotnet monorepo preset", - }, - { - matchPackagePatterns: ["^Microsoft.Extensions.Configuration"], - groupName: "Microsoft.Extensions.Configuration", - description: "Group Microsoft.Extensions.Configuration to exclude them from the dotnet monorepo preset", - }, - { - matchPackagePatterns: ["^Microsoft.Extensions.DependencyInjection"], - groupName: "Microsoft.Extensions.DependencyInjection", - description: "Group Microsoft.Extensions.DependencyInjection to exclude them from the dotnet monorepo preset", - }, { matchPackageNames: [ "AngleSharp", From 1228fe51c8db1f41be4731fd72e191cb88e60792 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 8 May 2025 07:49:16 -0400 Subject: [PATCH 04/20] Resolve auth warnings (#5784) --- .../TokenProviders/DuoUniversalTokenProvider.cs | 9 +++++---- src/Core/Core.csproj | 2 +- src/Identity/Identity.csproj | 2 -- .../Repositories/DeviceRepository.cs | 6 +----- .../Auth/Controllers/DevicesControllerTests.cs | 2 -- test/Identity.Test/Identity.Test.csproj | 2 -- .../Wrappers/BaseRequestValidatorTestWrapper.cs | 3 +++ .../Wrappers/UserManagerTestWrapper.cs | 16 ++++++++-------- 8 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs index 21311326c0..cbb994fa09 100644 --- a/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs @@ -16,10 +16,11 @@ public class DuoUniversalTokenProvider( IDuoUniversalTokenService duoUniversalTokenService) : IUserTwoFactorTokenProvider { /// - /// We need the IServiceProvider to resolve the IUserService. There is a complex dependency dance - /// occurring between IUserService, which extends the UserManager, and the usage of the - /// UserManager within this class. Trying to resolve the IUserService using the DI pipeline - /// will not allow the server to start and it will hang and give no helpful indication as to the problem. + /// We need the IServiceProvider to resolve the . There is a complex dependency dance + /// occurring between , which extends the , and the usage + /// of the within this class. Trying to resolve the using + /// the DI pipeline will not allow the server to start and it will hang and give no helpful indication as to the + /// problem. /// private readonly IServiceProvider _serviceProvider = serviceProvider; private readonly IDataProtectorTokenFactory _tokenDataFactory = tokenDataFactory; diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index c7e812fd2c..ba48b6175b 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -4,7 +4,7 @@ false bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml - $(WarningsNotAsErrors);CS1570;CS1574;CS9113;CS1998 + $(WarningsNotAsErrors);CS1574;CS9113;CS1998 diff --git a/src/Identity/Identity.csproj b/src/Identity/Identity.csproj index e9e188b53f..cb506d86e9 100644 --- a/src/Identity/Identity.csproj +++ b/src/Identity/Identity.csproj @@ -3,8 +3,6 @@ bitwarden-Identity false - - $(WarningsNotAsErrors);CS0162 diff --git a/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs b/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs index 723200ff1c..33643eba88 100644 --- a/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs @@ -17,15 +17,11 @@ public class DeviceRepository : Repository, IDeviceRepository private readonly IGlobalSettings _globalSettings; public DeviceRepository(GlobalSettings globalSettings) - : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + : base(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) { _globalSettings = globalSettings; } - public DeviceRepository(string connectionString, string readOnlyConnectionString) - : base(connectionString, readOnlyConnectionString) - { } - public async Task GetByIdAsync(Guid id, Guid userId) { var device = await GetByIdAsync(id); diff --git a/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs b/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs index 81e100c58c..540d23f98b 100644 --- a/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs @@ -8,7 +8,6 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Settings; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -23,7 +22,6 @@ public class DevicesControllerTest private readonly IUntrustDevicesCommand _untrustDevicesCommand; private readonly IUserRepository _userRepositoryMock; private readonly ICurrentContext _currentContextMock; - private readonly IGlobalSettings _globalSettingsMock; private readonly ILogger _loggerMock; private readonly DevicesController _sut; diff --git a/test/Identity.Test/Identity.Test.csproj b/test/Identity.Test/Identity.Test.csproj index 34010d811b..fc0cf07b63 100644 --- a/test/Identity.Test/Identity.Test.csproj +++ b/test/Identity.Test/Identity.Test.csproj @@ -2,8 +2,6 @@ false - - $(WarningsNotAsErrors);CS0672;CS1998 diff --git a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs index ed28f00ce7..c204e380b8 100644 --- a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs +++ b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs @@ -96,6 +96,7 @@ IBaseRequestValidatorTestWrapper return context.ValidatedTokenRequest.Subject ?? new ClaimsPrincipal(); } + [Obsolete] protected override void SetErrorResult( BaseRequestValidationContextFake context, Dictionary customResponse) @@ -103,6 +104,7 @@ IBaseRequestValidatorTestWrapper context.GrantResult = new GrantValidationResult(TokenRequestErrors.InvalidGrant, customResponse: customResponse); } + [Obsolete] protected override void SetSsoResult( BaseRequestValidationContextFake context, Dictionary customResponse) @@ -121,6 +123,7 @@ IBaseRequestValidatorTestWrapper return Task.CompletedTask; } + [Obsolete] protected override void SetTwoFactorResult( BaseRequestValidationContextFake context, Dictionary customResponse) diff --git a/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs b/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs index f1207a4b9a..3152f2327f 100644 --- a/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs +++ b/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs @@ -56,9 +56,9 @@ public class UserManagerTestWrapper : UserManager where TUser : cl /// /// /// - public override async Task GetTwoFactorEnabledAsync(TUser user) + public override Task GetTwoFactorEnabledAsync(TUser user) { - return TWO_FACTOR_ENABLED; + return Task.FromResult(TWO_FACTOR_ENABLED); } /// @@ -66,9 +66,9 @@ public class UserManagerTestWrapper : UserManager where TUser : cl /// /// /// - public override async Task> GetValidTwoFactorProvidersAsync(TUser user) + public override Task> GetValidTwoFactorProvidersAsync(TUser user) { - return TWO_FACTOR_PROVIDERS; + return Task.FromResult(TWO_FACTOR_PROVIDERS); } /// @@ -77,9 +77,9 @@ public class UserManagerTestWrapper : UserManager where TUser : cl /// /// /// - public override async Task GenerateTwoFactorTokenAsync(TUser user, string tokenProvider) + public override Task GenerateTwoFactorTokenAsync(TUser user, string tokenProvider) { - return TWO_FACTOR_TOKEN; + return Task.FromResult(TWO_FACTOR_TOKEN); } /// @@ -89,8 +89,8 @@ public class UserManagerTestWrapper : UserManager where TUser : cl /// /// /// - public override async Task VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token) + public override Task VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token) { - return TWO_FACTOR_TOKEN_VERIFIED; + return Task.FromResult(TWO_FACTOR_TOKEN_VERIFIED); } } From e4a93b24f13c72714085a35699bf71484fb78e20 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 8 May 2025 09:15:27 -0400 Subject: [PATCH 05/20] Resolve AC warnings (#5785) --- .../IConfirmOrganizationUserCommand.cs | 1 + .../Controllers/GroupsControllerPutTests.cs | 2 +- .../InviteOrganizationUserCommandTests.cs | 4 ++-- .../InviteOrganizationUsersValidatorTests.cs | 2 +- .../Services/EventRouteServiceTests.cs | 16 ++++++++-------- .../Services/SlackEventHandlerTests.cs | 6 +++--- 6 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs index 302ee0901d..e574d29e48 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs @@ -1,4 +1,5 @@ using Bit.Core.Entities; +using Bit.Core.Exceptions; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; diff --git a/test/Api.Test/AdminConsole/Controllers/GroupsControllerPutTests.cs b/test/Api.Test/AdminConsole/Controllers/GroupsControllerPutTests.cs index 0e260e73e6..71dc5e5aea 100644 --- a/test/Api.Test/AdminConsole/Controllers/GroupsControllerPutTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/GroupsControllerPutTests.cs @@ -304,7 +304,7 @@ public class GroupsControllerPutTests // Arrange repositories sutProvider.GetDependency().GetManyUserIdsByIdAsync(group.Id).Returns(currentGroupUsers ?? []); sutProvider.GetDependency().GetByIdWithCollectionsAsync(group.Id) - .Returns(new Tuple>(group, currentCollectionAccess ?? [])); + .Returns(new Tuple>(group, currentCollectionAccess ?? [])); if (savingUser != null) { sutProvider.GetDependency().GetByOrganizationAsync(orgId, savingUser.UserId!.Value) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs index 0592b481d3..80ce4cf481 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs @@ -677,7 +677,7 @@ public class InviteOrganizationUserCommandTests // Assert Assert.IsType>(result); - sutProvider.GetDependency().Received(1) + await sutProvider.GetDependency().Received(1) .SendOrganizationMaxSeatLimitReachedEmailAsync(organization, 2, Arg.Is>(emails => emails.Any(email => email == "provider@email.com"))); } @@ -768,7 +768,7 @@ public class InviteOrganizationUserCommandTests // Assert Assert.IsType>(result); - sutProvider.GetDependency().Received(1) + await sutProvider.GetDependency().Received(1) .SendOrganizationAutoscaledEmailAsync(organization, 1, Arg.Is>(emails => emails.Any(email => email == "provider@email.com"))); } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs index 191ef05603..ee40fb1152 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs @@ -61,7 +61,7 @@ public class InviteOrganizationUsersValidatorTests _ = await sutProvider.Sut.ValidateAsync(request); - sutProvider.GetDependency() + await sutProvider.GetDependency() .Received(1) .ValidateUpdateAsync(Arg.Is(x => x.SmSeatsChanged == true && x.SmSeats == 12)); diff --git a/test/Core.Test/AdminConsole/Services/EventRouteServiceTests.cs b/test/Core.Test/AdminConsole/Services/EventRouteServiceTests.cs index f593a4628b..1a42d846f2 100644 --- a/test/Core.Test/AdminConsole/Services/EventRouteServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventRouteServiceTests.cs @@ -26,8 +26,8 @@ public class EventRouteServiceTests await Subject.CreateAsync(eventMessage); - _broadcastEventWriteService.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any()); - _storageEventWriteService.Received(1).CreateAsync(eventMessage); + await _broadcastEventWriteService.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any()); + await _storageEventWriteService.Received(1).CreateAsync(eventMessage); } [Theory, BitAutoData] @@ -37,8 +37,8 @@ public class EventRouteServiceTests await Subject.CreateAsync(eventMessage); - _broadcastEventWriteService.Received(1).CreateAsync(eventMessage); - _storageEventWriteService.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any()); + await _broadcastEventWriteService.Received(1).CreateAsync(eventMessage); + await _storageEventWriteService.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any()); } [Theory, BitAutoData] @@ -48,8 +48,8 @@ public class EventRouteServiceTests await Subject.CreateManyAsync(eventMessages); - _broadcastEventWriteService.DidNotReceiveWithAnyArgs().CreateManyAsync(Arg.Any>()); - _storageEventWriteService.Received(1).CreateManyAsync(eventMessages); + await _broadcastEventWriteService.DidNotReceiveWithAnyArgs().CreateManyAsync(Arg.Any>()); + await _storageEventWriteService.Received(1).CreateManyAsync(eventMessages); } [Theory, BitAutoData] @@ -59,7 +59,7 @@ public class EventRouteServiceTests await Subject.CreateManyAsync(eventMessages); - _broadcastEventWriteService.Received(1).CreateManyAsync(eventMessages); - _storageEventWriteService.DidNotReceiveWithAnyArgs().CreateManyAsync(Arg.Any>()); + await _broadcastEventWriteService.Received(1).CreateManyAsync(eventMessages); + await _storageEventWriteService.DidNotReceiveWithAnyArgs().CreateManyAsync(Arg.Any>()); } } diff --git a/test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs b/test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs index 798ba219eb..558bded8b3 100644 --- a/test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs @@ -89,7 +89,7 @@ public class SlackEventHandlerTests var sutProvider = GetSutProvider(OneConfiguration()); await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( + await sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( Arg.Is(AssertHelper.AssertPropertyEqual(_token)), Arg.Is(AssertHelper.AssertPropertyEqual( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")), @@ -103,13 +103,13 @@ public class SlackEventHandlerTests var sutProvider = GetSutProvider(TwoConfigurations()); await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( + await sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( Arg.Is(AssertHelper.AssertPropertyEqual(_token)), Arg.Is(AssertHelper.AssertPropertyEqual( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")), Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)) ); - sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( + await sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( Arg.Is(AssertHelper.AssertPropertyEqual(_token2)), Arg.Is(AssertHelper.AssertPropertyEqual( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")), From c9b6e5de86298a4bf770ad5a9a7a383a06fab9d7 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 8 May 2025 10:43:19 -0400 Subject: [PATCH 06/20] [PM-20084] [PM-20086] Add `TrialLength` parameter to trial initiation endpoint and email (#5770) * Add trial length parameter to trial initiation endpoint and email * Add feature flag that pegs trial length to 7 when disabled * Add optionality to Identity * Move feature service injection to identity accounts controller --- .../TrialSendVerificationEmailRequestModel.cs | 1 + .../Models/Mail/TrialInititaionVerifyEmail.cs | 16 +++++++++++++++- ...ialInitiationEmailForRegistrationCommand.cs | 3 ++- ...ialInitiationEmailForRegistrationCommand.cs | 10 ++++++++-- src/Core/Constants.cs | 1 + src/Core/Enums/EnumExtensions.cs | 18 ++++++++++++++++++ .../TrialInitiationVerifyEmail.html.hbs | 2 +- .../TrialInitiationVerifyEmail.text.hbs | 2 +- src/Core/Services/IMailService.cs | 3 ++- .../Implementations/HandlebarsMailService.cs | 6 ++++-- .../NoopImplementations/NoopMailService.cs | 3 ++- .../Billing/Controller/AccountsController.cs | 14 +++++++++++--- src/Identity/Startup.cs | 1 + 13 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 src/Core/Enums/EnumExtensions.cs diff --git a/src/Core/Billing/Models/Api/Requests/Accounts/TrialSendVerificationEmailRequestModel.cs b/src/Core/Billing/Models/Api/Requests/Accounts/TrialSendVerificationEmailRequestModel.cs index 2e8780e6a3..b31da9efbc 100644 --- a/src/Core/Billing/Models/Api/Requests/Accounts/TrialSendVerificationEmailRequestModel.cs +++ b/src/Core/Billing/Models/Api/Requests/Accounts/TrialSendVerificationEmailRequestModel.cs @@ -7,4 +7,5 @@ public class TrialSendVerificationEmailRequestModel : RegisterSendVerificationEm { public ProductTierType ProductTier { get; set; } public IEnumerable Products { get; set; } + public int? TrialLength { get; set; } } diff --git a/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs b/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs index 33b9578d0e..b97390dcc9 100644 --- a/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs +++ b/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs @@ -1,5 +1,6 @@ using Bit.Core.Auth.Models.Mail; using Bit.Core.Billing.Enums; +using Bit.Core.Enums; namespace Bit.Core.Billing.Models.Mail; @@ -16,13 +17,26 @@ public class TrialInitiationVerifyEmail : RegisterVerifyEmail $"&email={Email}" + $"&fromEmail=true" + $"&productTier={(int)ProductTier}" + - $"&product={string.Join(",", Product.Select(p => (int)p))}"; + $"&product={string.Join(",", Product.Select(p => (int)p))}" + + $"&trialLength={TrialLength}"; } + public string VerifyYourEmailHTMLCopy => + TrialLength == 7 + ? "Verify your email address below to finish signing up for your free trial." + : $"Verify your email address below to finish signing up for your {ProductTier.GetDisplayName()} plan."; + + public string VerifyYourEmailTextCopy => + TrialLength == 7 + ? "Verify your email address using the link below and start your free trial of Bitwarden." + : $"Verify your email address using the link below and start your {ProductTier.GetDisplayName()} Bitwarden plan."; + public ProductTierType ProductTier { get; set; } public IEnumerable Product { get; set; } + public int TrialLength { get; set; } + /// /// Currently we only support one product type at a time, despite Product being a collection. /// If we receive both PasswordManager and SecretsManager, we'll send the user to the PM trial route diff --git a/src/Core/Billing/TrialInitiation/Registration/ISendTrialInitiationEmailForRegistrationCommand.cs b/src/Core/Billing/TrialInitiation/Registration/ISendTrialInitiationEmailForRegistrationCommand.cs index 01550228be..6ec31d7b8f 100644 --- a/src/Core/Billing/TrialInitiation/Registration/ISendTrialInitiationEmailForRegistrationCommand.cs +++ b/src/Core/Billing/TrialInitiation/Registration/ISendTrialInitiationEmailForRegistrationCommand.cs @@ -10,5 +10,6 @@ public interface ISendTrialInitiationEmailForRegistrationCommand string? name, bool receiveMarketingEmails, ProductTierType productTier, - IEnumerable products); + IEnumerable products, + int trialLength); } diff --git a/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs b/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs index 385d7ebbd6..3e5b056ec6 100644 --- a/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs +++ b/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs @@ -22,7 +22,8 @@ public class SendTrialInitiationEmailForRegistrationCommand( string? name, bool receiveMarketingEmails, ProductTierType productTier, - IEnumerable products) + IEnumerable products, + int trialLength) { ArgumentException.ThrowIfNullOrWhiteSpace(email, nameof(email)); @@ -43,7 +44,12 @@ public class SendTrialInitiationEmailForRegistrationCommand( await PerformConstantTimeOperationsAsync(); - await mailService.SendTrialInitiationSignupEmailAsync(userExists, email, token, productTier, products); + if (trialLength != 0 && trialLength != 7) + { + trialLength = 7; + } + + await mailService.SendTrialInitiationSignupEmailAsync(userExists, email, token, productTier, products, trialLength); return null; } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 90e9e46619..f12e804a61 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -150,6 +150,7 @@ public static class FeatureFlagKeys public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically"; public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup"; public const string UseOrganizationWarningsService = "use-organization-warnings-service"; + public const string PM20322_AllowTrialLength0 = "pm-20322-allow-trial-length-0"; /* Data Insights and Reporting Team */ public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; diff --git a/src/Core/Enums/EnumExtensions.cs b/src/Core/Enums/EnumExtensions.cs new file mode 100644 index 0000000000..d60b530ffb --- /dev/null +++ b/src/Core/Enums/EnumExtensions.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using System.Reflection; + +namespace Bit.Core.Enums; + +public static class EnumExtensions +{ + public static string GetDisplayName(this Enum value) + { + var field = value.GetType().GetField(value.ToString()); + if (field?.GetCustomAttribute() is { } attribute) + { + return attribute.Name ?? value.ToString(); + } + + return value.ToString(); + } +} diff --git a/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.html.hbs b/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.html.hbs index 6c1b9edec0..5d379288ef 100644 --- a/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.html.hbs +++ b/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.html.hbs @@ -2,7 +2,7 @@ diff --git a/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.text.hbs b/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.text.hbs index 690cf77734..4e0d064e36 100644 --- a/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.text.hbs +++ b/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.text.hbs @@ -1,5 +1,5 @@ {{#>BasicTextLayout}} -Verify your email address using the link below and start your free trial of Bitwarden. +{{VerifyYourEmailTextCopy}} If you did not request this email from Bitwarden, you can safely ignore it. diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 9b05810eaa..11d9603a07 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -21,7 +21,8 @@ public interface IMailService string email, string token, ProductTierType productTier, - IEnumerable products); + IEnumerable products, + int trialLength); Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token); Task SendCannotDeleteClaimedAccountEmailAsync(string email); Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail); diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 1fca85eff4..3266cc9c2e 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -84,7 +84,8 @@ public class HandlebarsMailService : IMailService string email, string token, ProductTierType productTier, - IEnumerable products) + IEnumerable products, + int trialLength) { var message = CreateDefaultMessage("Verify your email", email); var model = new TrialInitiationVerifyEmail @@ -95,7 +96,8 @@ public class HandlebarsMailService : IMailService WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, SiteName = _globalSettings.SiteName, ProductTier = productTier, - Product = products + Product = products, + TrialLength = trialLength }; await AddMessageContentAsync(message, "Billing.TrialInitiationVerifyEmail", model); message.MetaData.Add("SendGridBypassListManagement", true); diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index cd5c1af8a8..bbad5965f4 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -33,7 +33,8 @@ public class NoopMailService : IMailService string email, string token, ProductTierType productTier, - IEnumerable products) + IEnumerable products, + int trailLength) { return Task.FromResult(0); } diff --git a/src/Identity/Billing/Controller/AccountsController.cs b/src/Identity/Billing/Controller/AccountsController.cs index 96ec1280cd..b83940d3aa 100644 --- a/src/Identity/Billing/Controller/AccountsController.cs +++ b/src/Identity/Billing/Controller/AccountsController.cs @@ -1,6 +1,8 @@ -using Bit.Core.Billing.Models.Api.Requests.Accounts; +using Bit.Core; +using Bit.Core.Billing.Models.Api.Requests.Accounts; using Bit.Core.Billing.TrialInitiation.Registration; using Bit.Core.Context; +using Bit.Core.Services; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Services; @@ -15,18 +17,24 @@ namespace Bit.Identity.Billing.Controller; public class AccountsController( ICurrentContext currentContext, ISendTrialInitiationEmailForRegistrationCommand sendTrialInitiationEmailForRegistrationCommand, - IReferenceEventService referenceEventService) : Microsoft.AspNetCore.Mvc.Controller + IReferenceEventService referenceEventService, + IFeatureService featureService) : Microsoft.AspNetCore.Mvc.Controller { [HttpPost("trial/send-verification-email")] [SelfHosted(NotSelfHostedOnly = true)] public async Task PostTrialInitiationSendVerificationEmailAsync([FromBody] TrialSendVerificationEmailRequestModel model) { + var allowTrialLength0 = featureService.IsEnabled(FeatureFlagKeys.PM20322_AllowTrialLength0); + + var trialLength = allowTrialLength0 ? model.TrialLength ?? 7 : 7; + var token = await sendTrialInitiationEmailForRegistrationCommand.Handle( model.Email, model.Name, model.ReceiveMarketingEmails, model.ProductTier, - model.Products); + model.Products, + trialLength); var refEvent = new ReferenceEvent { diff --git a/src/Identity/Startup.cs b/src/Identity/Startup.cs index 320c91b248..2d8ca55def 100644 --- a/src/Identity/Startup.cs +++ b/src/Identity/Startup.cs @@ -145,6 +145,7 @@ public class Startup // Services services.AddBaseServices(globalSettings); services.AddDefaultServices(globalSettings); + services.AddOptionality(); services.AddCoreLocalizationServices(); services.AddBillingOperations(); From af08d4b2a5ff5d16a3c8f4fbbc305d03870d6940 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Thu, 8 May 2025 11:27:06 -0400 Subject: [PATCH 07/20] chore(workflows): Update image tag logic to handle forked branches --- .github/workflows/build.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 33edd075a0..5077f1ba32 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,6 +14,7 @@ on: env: _AZ_REGISTRY: "bitwardenprod.azurecr.io" + _GITHUB_PR_REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }} jobs: lint: @@ -234,12 +235,18 @@ jobs: - name: Generate Docker image tag id: tag run: | - if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then - IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s#/#-#g") + if [[ "${GITHUB_EVENT_NAME}" == "pull_request" || "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then + IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize branch name to alphanumeric only else IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") fi + if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then + SANITIZED_REPO_NAME=$(echo "$_GITHUB_PR_REPO_NAME" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize repo name to alphanumeric only + IMAGE_TAG=$SANITIZED_REPO_NAME-$IMAGE_TAG # Add repo name to the tag + IMAGE_TAG=${IMAGE_TAG:0:128} # Limit to 128 characters, as that's the max length for Docker image tags + fi + if [[ "$IMAGE_TAG" == "main" ]]; then IMAGE_TAG=dev fi From e3f6562d3a8e483c205a6f498ed4a74d56837d59 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 8 May 2025 14:07:35 -0400 Subject: [PATCH 08/20] [PM-21345] Re-add existing customer coupon after subscription update (#5788) * Re-add existing customer coupon after subscription update * Run dotnet format --- .../Implementations/StripePaymentService.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 51be369527..85ad7d64d7 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -112,6 +112,8 @@ public class StripePaymentService : IPaymentService throw new BadRequestException("You do not have an active subscription. Reinstate your subscription to make changes."); } + var existingCoupon = sub.Customer.Discount?.Coupon?.Id; + var collectionMethod = sub.CollectionMethod; var daysUntilDue = sub.DaysUntilDue; var chargeNow = collectionMethod == "charge_automatically"; @@ -216,6 +218,19 @@ public class StripePaymentService : IPaymentService DaysUntilDue = daysUntilDue, }); } + + var customer = await _stripeAdapter.CustomerGetAsync(sub.CustomerId); + + var newCoupon = customer.Discount?.Coupon?.Id; + + if (!string.IsNullOrEmpty(existingCoupon) && string.IsNullOrEmpty(newCoupon)) + { + // Re-add the lost coupon due to the update. + await _stripeAdapter.CustomerUpdateAsync(sub.CustomerId, new CustomerUpdateOptions + { + Coupon = existingCoupon + }); + } } return paymentIntentClientSecret; From 547df250455f350d8d76dd5403c45466b14ac1bb Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Thu, 8 May 2025 15:57:24 -0400 Subject: [PATCH 09/20] chore(feature-flag): [PM-12433] Remove device-trust-logging feature flag * Completed grouping of feature flags by team. * Completed grouping feature flags by team. * Remove email delay feature flag * Removed feature flag * Fixed reference. * Remove flag after merge. * Removed flag from server. * Removed feature flag from server --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index f12e804a61..a27738fd19 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -114,7 +114,6 @@ public static class FeatureFlagKeys public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence"; public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; public const string EmailVerification = "email-verification"; - public const string DeviceTrustLogging = "pm-8285-device-trust-logging"; public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh"; public const string NewDeviceVerification = "new-device-verification"; public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor"; From 5b3d3d6e205c6d78f31586ee10ea5a8bc68b3171 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 9 May 2025 10:46:49 +1000 Subject: [PATCH 10/20] CommandResult and ValidationResult tweaks (#5772) * Simplify and align CommandResult and ValidationResult. In particular, 1 error per Failure/Invalid. * Move these files to a common namespace * Remove unused code --- .../src/Scim/Users/PostUserCommand.cs | 7 +- src/Api/Utilities/CommandResultExtensions.cs | 31 ----- ...vokeNonCompliantOrganizationUserCommand.cs | 2 +- .../InviteUsers/Errors/ErrorMapper.cs | 2 +- .../Errors/FailedToInviteUsersError.cs | 4 +- .../Errors/NoUsersToInviteError.cs | 4 +- .../Errors/UserAlreadyExistsError.cs | 4 +- .../IInviteOrganizationUsersCommand.cs | 2 +- .../InviteOrganizationUsersCommand.cs | 12 +- .../CannotAutoScaleOnSelfHostError.cs | 2 +- .../InviteUsersEnvironmentValidator.cs | 2 +- .../InviteOrganizationUserValidator.cs | 6 +- .../Validation/Organization/Errors.cs | 4 +- .../InviteUsersOrganizationValidator.cs | 2 +- .../Validation/PasswordManager/Errors.cs | 2 +- .../InviteUsersPasswordManagerValidator.cs | 2 +- .../InviteUsers/Validation/Payments/Errors.cs | 4 +- .../Payments/InviteUserPaymentValidation.cs | 2 +- .../InviteUsers/Validation/Provider/Errors.cs | 2 +- ...vitingUserOrganizationProviderValidator.cs | 2 +- ...vokeNonCompliantOrganizationUserCommand.cs | 2 +- .../Shared/Validation/ValidationResult.cs | 44 ------- .../Utilities/Commands/CommandResult.cs | 51 +++++++++ .../{ => Utilities}/Errors/Error.cs | 2 +- .../Errors/InsufficientPermissionsError.cs | 2 +- .../Errors/InvalidResultTypeError.cs | 2 +- .../Errors/RecordNotFoundError.cs | 2 +- .../Validation/IValidator.cs | 2 +- .../Utilities/Validation/ValidationResult.cs | 20 ++++ src/Core/Models/Commands/BadRequestFailure.cs | 23 ---- src/Core/Models/Commands/CommandResult.cs | 88 -------------- .../Models/Commands/NoRecordFoundFailure.cs | 24 ---- .../Utilities/CommandResultExtensionTests.cs | 107 ------------------ .../InviteOrganizationUserCommandTests.cs | 12 +- .../InviteOrganizationUsersValidatorTests.cs | 4 +- .../InviteUserOrganizationValidationTests.cs | 6 +- .../InviteUserPaymentValidationTests.cs | 4 +- ...PasswordManagerInviteUserValidatorTests.cs | 6 +- .../SingleOrgPolicyValidatorTests.cs | 2 +- ...actorAuthenticationPolicyValidatorTests.cs | 2 +- .../AdminConsole/Shared/IValidatorTests.cs | 14 +-- .../Utilities}/Commands/CommandResultTests.cs | 6 +- 42 files changed, 137 insertions(+), 386 deletions(-) delete mode 100644 src/Api/Utilities/CommandResultExtensions.cs delete mode 100644 src/Core/AdminConsole/Shared/Validation/ValidationResult.cs create mode 100644 src/Core/AdminConsole/Utilities/Commands/CommandResult.cs rename src/Core/AdminConsole/{ => Utilities}/Errors/Error.cs (80%) rename src/Core/AdminConsole/{ => Utilities}/Errors/InsufficientPermissionsError.cs (83%) rename src/Core/AdminConsole/{ => Utilities}/Errors/InvalidResultTypeError.cs (71%) rename src/Core/AdminConsole/{ => Utilities}/Errors/RecordNotFoundError.cs (82%) rename src/Core/AdminConsole/{Shared => Utilities}/Validation/IValidator.cs (62%) create mode 100644 src/Core/AdminConsole/Utilities/Validation/ValidationResult.cs delete mode 100644 src/Core/Models/Commands/BadRequestFailure.cs delete mode 100644 src/Core/Models/Commands/CommandResult.cs delete mode 100644 src/Core/Models/Commands/NoRecordFoundFailure.cs delete mode 100644 test/Api.Test/Utilities/CommandResultExtensionTests.cs rename test/Core.Test/{Models => AdminConsole/Utilities}/Commands/CommandResultTests.cs (92%) diff --git a/bitwarden_license/src/Scim/Users/PostUserCommand.cs b/bitwarden_license/src/Scim/Users/PostUserCommand.cs index 46116a46ae..5b4a0c29cd 100644 --- a/bitwarden_license/src/Scim/Users/PostUserCommand.cs +++ b/bitwarden_license/src/Scim/Users/PostUserCommand.cs @@ -6,10 +6,10 @@ using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.Utilities.Commands; using Bit.Core.Billing.Pricing; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Commands; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; @@ -76,9 +76,8 @@ public class PostUserCommand( var invitedOrganizationUserId = result switch { Success success => success.Value.InvitedUser.Id, - Failure failure when failure.Errors - .Any(x => x.Message == NoUsersToInviteError.Code) => (Guid?)null, - Failure failure when failure.Errors.Length != 0 => throw MapToBitException(failure.Errors), + Failure { Error.Message: NoUsersToInviteError.Code } => (Guid?)null, + Failure failure => throw MapToBitException(failure.Error), _ => throw new InvalidOperationException() }; diff --git a/src/Api/Utilities/CommandResultExtensions.cs b/src/Api/Utilities/CommandResultExtensions.cs deleted file mode 100644 index c7315a0fa0..0000000000 --- a/src/Api/Utilities/CommandResultExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Bit.Core.Models.Commands; -using Microsoft.AspNetCore.Mvc; - -namespace Bit.Api.Utilities; - -public static class CommandResultExtensions -{ - public static IActionResult MapToActionResult(this CommandResult commandResult) - { - return commandResult switch - { - NoRecordFoundFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound }, - BadRequestFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, - Failure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, - Success success => new ObjectResult(success.Value) { StatusCode = StatusCodes.Status200OK }, - _ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}") - }; - } - - public static IActionResult MapToActionResult(this CommandResult commandResult) - { - return commandResult switch - { - NoRecordFoundFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound }, - BadRequestFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, - Failure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, - Success => new ObjectResult(new { }) { StatusCode = StatusCodes.Status200OK }, - _ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}") - }; - } -} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeNonCompliantOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeNonCompliantOrganizationUserCommand.cs index c9768a8905..024d56e8c3 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeNonCompliantOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeNonCompliantOrganizationUserCommand.cs @@ -1,5 +1,5 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; -using Bit.Core.Models.Commands; +using Bit.Core.AdminConsole.Utilities.Commands; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/ErrorMapper.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/ErrorMapper.cs index c66d366de5..38fa35b29a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/ErrorMapper.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/ErrorMapper.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Errors; +using Bit.Core.AdminConsole.Utilities.Errors; using Bit.Core.Exceptions; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/FailedToInviteUsersError.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/FailedToInviteUsersError.cs index 810ef744c9..48faf4cac0 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/FailedToInviteUsersError.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/FailedToInviteUsersError.cs @@ -1,5 +1,5 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/NoUsersToInviteError.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/NoUsersToInviteError.cs index 52697572e6..8cd70391a2 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/NoUsersToInviteError.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/NoUsersToInviteError.cs @@ -1,5 +1,5 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/UserAlreadyExistsError.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/UserAlreadyExistsError.cs index 475ad4a886..4fbb8f2bad 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/UserAlreadyExistsError.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/UserAlreadyExistsError.cs @@ -1,5 +1,5 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs index 3e4c7652a5..7e0a8dc3cd 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs @@ -1,5 +1,5 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; -using Bit.Core.Models.Commands; +using Bit.Core.AdminConsole.Utilities.Commands; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs index 662ed314ce..072bc5fc05 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs @@ -1,17 +1,17 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; -using Bit.Core.AdminConsole.Errors; using Bit.Core.AdminConsole.Interfaces; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Commands; +using Bit.Core.AdminConsole.Utilities.Errors; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models.Business; -using Bit.Core.Models.Commands; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; @@ -50,11 +50,11 @@ public class InviteOrganizationUsersCommand(IEventService eventService, { case Failure failure: return new Failure( - failure.Errors.Select(error => new Error(error.Message, + new Error(failure.Error.Message, new ScimInviteOrganizationUsersResponse { - InvitedUser = error.ErroredValue.InvitedUsers.FirstOrDefault() - }))); + InvitedUser = failure.Error.ErroredValue.InvitedUsers.FirstOrDefault() + })); case Success success when success.Value.InvitedUsers.Any(): var user = success.Value.InvitedUsers.First(); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/CannotAutoScaleOnSelfHostError.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/CannotAutoScaleOnSelfHostError.cs index 0624ffe027..e7e331686d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/CannotAutoScaleOnSelfHostError.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/CannotAutoScaleOnSelfHostError.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Errors; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/InviteUsersEnvironmentValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/InviteUsersEnvironmentValidator.cs index fd0441753a..fb50fd58dd 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/InviteUsersEnvironmentValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/InviteUsersEnvironmentValidator.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs index 79a3487d19..54f26cb46a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs @@ -1,7 +1,7 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Errors; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Models.Business; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/Errors.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/Errors.cs index 5d072ca17d..f9e9f4eebf 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/Errors.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/Errors.cs @@ -1,5 +1,5 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.Models.Business; +using Bit.Core.AdminConsole.Models.Business; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/InviteUsersOrganizationValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/InviteUsersOrganizationValidator.cs index 9e2ca8d9a6..ce617a2db3 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/InviteUsersOrganizationValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/InviteUsersOrganizationValidator.cs @@ -1,5 +1,5 @@ using Bit.Core.AdminConsole.Models.Business; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/Errors.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/Errors.cs index 6ff7181456..40afa5e9d0 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/Errors.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/Errors.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Errors; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/InviteUsersPasswordManagerValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/InviteUsersPasswordManagerValidator.cs index 6a8ec8e6d3..a1536ad439 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/InviteUsersPasswordManagerValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/InviteUsersPasswordManagerValidator.cs @@ -4,7 +4,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.V using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/Errors.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/Errors.cs index c74d1048ad..865a3cb83a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/Errors.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/Errors.cs @@ -1,5 +1,5 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/InviteUserPaymentValidation.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/InviteUserPaymentValidation.cs index cc17a673f9..496dddc916 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/InviteUserPaymentValidation.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/InviteUserPaymentValidation.cs @@ -1,6 +1,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/Errors.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/Errors.cs index 104ce5cc7e..759ac1b780 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/Errors.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/Errors.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Errors; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/InvitingUserOrganizationProviderValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/InvitingUserOrganizationProviderValidator.cs index f84b25f76f..eeb19eec98 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/InvitingUserOrganizationProviderValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/InvitingUserOrganizationProviderValidator.cs @@ -1,5 +1,5 @@ using Bit.Core.AdminConsole.Enums.Provider; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Extensions; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs index 971ed02b29..0773cf4f9c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs @@ -1,8 +1,8 @@ using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; +using Bit.Core.AdminConsole.Utilities.Commands; using Bit.Core.Enums; -using Bit.Core.Models.Commands; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; diff --git a/src/Core/AdminConsole/Shared/Validation/ValidationResult.cs b/src/Core/AdminConsole/Shared/Validation/ValidationResult.cs deleted file mode 100644 index ba78601637..0000000000 --- a/src/Core/AdminConsole/Shared/Validation/ValidationResult.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Bit.Core.AdminConsole.Errors; - -namespace Bit.Core.AdminConsole.Shared.Validation; - -public abstract record ValidationResult; - -public record Valid : ValidationResult -{ - public Valid() { } - - public Valid(T Value) - { - this.Value = Value; - } - - public T Value { get; init; } -} - -public record Invalid : ValidationResult -{ - public IEnumerable> Errors { get; init; } = []; - - public string ErrorMessageString => string.Join(" ", Errors.Select(e => e.Message)); - - public Invalid() { } - - public Invalid(Error error) : this([error]) { } - - public Invalid(IEnumerable> errors) - { - Errors = errors; - } -} - -public static class ValidationResultMappers -{ - public static ValidationResult Map(this ValidationResult validationResult, B invalidValue) => - validationResult switch - { - Valid => new Valid(invalidValue), - Invalid invalid => new Invalid(invalid.Errors.Select(x => x.ToError(invalidValue))), - _ => throw new ArgumentOutOfRangeException(nameof(validationResult), "Unhandled validation result type") - }; -} diff --git a/src/Core/AdminConsole/Utilities/Commands/CommandResult.cs b/src/Core/AdminConsole/Utilities/Commands/CommandResult.cs new file mode 100644 index 0000000000..274b1a8ba5 --- /dev/null +++ b/src/Core/AdminConsole/Utilities/Commands/CommandResult.cs @@ -0,0 +1,51 @@ +#nullable enable + +using Bit.Core.AdminConsole.Utilities.Errors; +using Bit.Core.AdminConsole.Utilities.Validation; + +namespace Bit.Core.AdminConsole.Utilities.Commands; + +public abstract class CommandResult; + +public class Success(T value) : CommandResult +{ + public T Value { get; } = value; +} + +public class Failure(Error error) : CommandResult +{ + public Error Error { get; } = error; +} + +public class Partial(IEnumerable successfulItems, IEnumerable> failedItems) + : CommandResult +{ + public IEnumerable Successes { get; } = successfulItems; + public IEnumerable> Failures { get; } = failedItems; +} + +public static class CommandResultExtensions +{ + /// + /// This is to help map between the InvalidT ValidationResult and the FailureT CommandResult types. + /// + /// + /// This is the invalid type from validating the object. + /// This function will map between the two types for the inner ErrorT + /// Invalid object's type + /// Failure object's type + /// + public static CommandResult MapToFailure(this Invalid invalidResult, Func mappingFunction) => + new Failure(invalidResult.Error.ToError(mappingFunction(invalidResult.Error.ErroredValue))); +} + +[Obsolete("Use CommandResult instead. This will be removed once old code is updated.")] +public class CommandResult(IEnumerable errors) +{ + public CommandResult(string error) : this([error]) { } + + public bool Success => ErrorMessages.Count == 0; + public bool HasErrors => ErrorMessages.Count > 0; + public List ErrorMessages { get; } = errors.ToList(); + public CommandResult() : this(Array.Empty()) { } +} diff --git a/src/Core/AdminConsole/Errors/Error.cs b/src/Core/AdminConsole/Utilities/Errors/Error.cs similarity index 80% rename from src/Core/AdminConsole/Errors/Error.cs rename to src/Core/AdminConsole/Utilities/Errors/Error.cs index 7ad057d6ed..949c6903a0 100644 --- a/src/Core/AdminConsole/Errors/Error.cs +++ b/src/Core/AdminConsole/Utilities/Errors/Error.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Errors; +namespace Bit.Core.AdminConsole.Utilities.Errors; public record Error(string Message, T ErroredValue); diff --git a/src/Core/AdminConsole/Errors/InsufficientPermissionsError.cs b/src/Core/AdminConsole/Utilities/Errors/InsufficientPermissionsError.cs similarity index 83% rename from src/Core/AdminConsole/Errors/InsufficientPermissionsError.cs rename to src/Core/AdminConsole/Utilities/Errors/InsufficientPermissionsError.cs index d04ceba7c9..c1a524fa0b 100644 --- a/src/Core/AdminConsole/Errors/InsufficientPermissionsError.cs +++ b/src/Core/AdminConsole/Utilities/Errors/InsufficientPermissionsError.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Errors; +namespace Bit.Core.AdminConsole.Utilities.Errors; public record InsufficientPermissionsError(string Message, T ErroredValue) : Error(Message, ErroredValue) { diff --git a/src/Core/AdminConsole/Errors/InvalidResultTypeError.cs b/src/Core/AdminConsole/Utilities/Errors/InvalidResultTypeError.cs similarity index 71% rename from src/Core/AdminConsole/Errors/InvalidResultTypeError.cs rename to src/Core/AdminConsole/Utilities/Errors/InvalidResultTypeError.cs index 67b5b634fb..f39aea68ce 100644 --- a/src/Core/AdminConsole/Errors/InvalidResultTypeError.cs +++ b/src/Core/AdminConsole/Utilities/Errors/InvalidResultTypeError.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Errors; +namespace Bit.Core.AdminConsole.Utilities.Errors; public record InvalidResultTypeError(T Value) : Error(Code, Value) { diff --git a/src/Core/AdminConsole/Errors/RecordNotFoundError.cs b/src/Core/AdminConsole/Utilities/Errors/RecordNotFoundError.cs similarity index 82% rename from src/Core/AdminConsole/Errors/RecordNotFoundError.cs rename to src/Core/AdminConsole/Utilities/Errors/RecordNotFoundError.cs index 25a169efe1..748bb57b5f 100644 --- a/src/Core/AdminConsole/Errors/RecordNotFoundError.cs +++ b/src/Core/AdminConsole/Utilities/Errors/RecordNotFoundError.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Errors; +namespace Bit.Core.AdminConsole.Utilities.Errors; public record RecordNotFoundError(string Message, T ErroredValue) : Error(Message, ErroredValue) { diff --git a/src/Core/AdminConsole/Shared/Validation/IValidator.cs b/src/Core/AdminConsole/Utilities/Validation/IValidator.cs similarity index 62% rename from src/Core/AdminConsole/Shared/Validation/IValidator.cs rename to src/Core/AdminConsole/Utilities/Validation/IValidator.cs index d90386f00e..1598e4472f 100644 --- a/src/Core/AdminConsole/Shared/Validation/IValidator.cs +++ b/src/Core/AdminConsole/Utilities/Validation/IValidator.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Shared.Validation; +namespace Bit.Core.AdminConsole.Utilities.Validation; public interface IValidator { diff --git a/src/Core/AdminConsole/Utilities/Validation/ValidationResult.cs b/src/Core/AdminConsole/Utilities/Validation/ValidationResult.cs new file mode 100644 index 0000000000..c62aa880ec --- /dev/null +++ b/src/Core/AdminConsole/Utilities/Validation/ValidationResult.cs @@ -0,0 +1,20 @@ +using Bit.Core.AdminConsole.Utilities.Errors; + +namespace Bit.Core.AdminConsole.Utilities.Validation; + +public abstract record ValidationResult; + +public record Valid(T Value) : ValidationResult; + +public record Invalid(Error Error) : ValidationResult; + +public static class ValidationResultMappers +{ + public static ValidationResult Map(this ValidationResult validationResult, B invalidValue) => + validationResult switch + { + Valid => new Valid(invalidValue), + Invalid invalid => new Invalid(invalid.Error.ToError(invalidValue)), + _ => throw new ArgumentOutOfRangeException(nameof(validationResult), "Unhandled validation result type") + }; +} diff --git a/src/Core/Models/Commands/BadRequestFailure.cs b/src/Core/Models/Commands/BadRequestFailure.cs deleted file mode 100644 index bd2753d4e4..0000000000 --- a/src/Core/Models/Commands/BadRequestFailure.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Bit.Core.Models.Commands; - -public class BadRequestFailure : Failure -{ - public BadRequestFailure(IEnumerable errorMessage) : base(errorMessage) - { - } - - public BadRequestFailure(string errorMessage) : base(errorMessage) - { - } -} - -public class BadRequestFailure : Failure -{ - public BadRequestFailure(IEnumerable errorMessage) : base(errorMessage) - { - } - - public BadRequestFailure(string errorMessage) : base(errorMessage) - { - } -} diff --git a/src/Core/Models/Commands/CommandResult.cs b/src/Core/Models/Commands/CommandResult.cs deleted file mode 100644 index 4a9477067e..0000000000 --- a/src/Core/Models/Commands/CommandResult.cs +++ /dev/null @@ -1,88 +0,0 @@ -#nullable enable - -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.Shared.Validation; - -namespace Bit.Core.Models.Commands; - -public class CommandResult(IEnumerable errors) -{ - public CommandResult(string error) : this([error]) { } - - public bool Success => ErrorMessages.Count == 0; - public bool HasErrors => ErrorMessages.Count > 0; - public List ErrorMessages { get; } = errors.ToList(); - public CommandResult() : this(Array.Empty()) { } -} - -public class Failure : CommandResult -{ - protected Failure(IEnumerable errorMessages) : base(errorMessages) - { - - } - public Failure(string errorMessage) : base(errorMessage) - { - - } -} - -public class Success : CommandResult -{ -} - -public abstract class CommandResult; - -public class Success(T value) : CommandResult -{ - public T Value { get; } = value; -} - -public class Failure(IEnumerable errorMessages) : CommandResult -{ - public List ErrorMessages { get; } = errorMessages.ToList(); - public Error[] Errors { get; set; } = []; - - public string ErrorMessage => string.Join(" ", ErrorMessages); - - public Failure(string error) : this([error]) - { - } - - public Failure(IEnumerable> errors) : this(errors.Select(e => e.Message)) - { - Errors = errors.ToArray(); - } - - public Failure(Error error) : this([error.Message]) - { - Errors = [error]; - } -} - -public class Partial : CommandResult -{ - public T[] Successes { get; set; } = []; - public Error[] Failures { get; set; } = []; - - public Partial(IEnumerable successfulItems, IEnumerable> failedItems) - { - Successes = successfulItems.ToArray(); - Failures = failedItems.ToArray(); - } -} - -public static class CommandResultExtensions -{ - /// - /// This is to help map between the InvalidT ValidationResult and the FailureT CommandResult types. - /// - /// - /// This is the invalid type from validating the object. - /// This function will map between the two types for the inner ErrorT - /// Invalid object's type - /// Failure object's type - /// - public static CommandResult MapToFailure(this Invalid invalidResult, Func mappingFunction) => - new Failure(invalidResult.Errors.Select(errorA => errorA.ToError(mappingFunction(errorA.ErroredValue)))); -} diff --git a/src/Core/Models/Commands/NoRecordFoundFailure.cs b/src/Core/Models/Commands/NoRecordFoundFailure.cs deleted file mode 100644 index a8a322b928..0000000000 --- a/src/Core/Models/Commands/NoRecordFoundFailure.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Bit.Core.Models.Commands; - -public class NoRecordFoundFailure : Failure -{ - public NoRecordFoundFailure(IEnumerable errorMessage) : base(errorMessage) - { - } - - public NoRecordFoundFailure(string errorMessage) : base(errorMessage) - { - } -} - -public class NoRecordFoundFailure : Failure -{ - public NoRecordFoundFailure(IEnumerable errorMessage) : base(errorMessage) - { - } - - public NoRecordFoundFailure(string errorMessage) : base(errorMessage) - { - } -} - diff --git a/test/Api.Test/Utilities/CommandResultExtensionTests.cs b/test/Api.Test/Utilities/CommandResultExtensionTests.cs deleted file mode 100644 index dafae10b5b..0000000000 --- a/test/Api.Test/Utilities/CommandResultExtensionTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -using Bit.Api.Utilities; -using Bit.Core.Models.Commands; -using Bit.Core.Vault.Entities; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Xunit; - -namespace Bit.Api.Test.Utilities; - -public class CommandResultExtensionTests -{ - public static IEnumerable WithGenericTypeTestCases() - { - yield return new object[] - { - new NoRecordFoundFailure(new[] { "Error 1", "Error 2" }), - new ObjectResult(new[] { "Error 1", "Error 2" }) { StatusCode = StatusCodes.Status404NotFound } - }; - yield return new object[] - { - new BadRequestFailure("Error 3"), - new ObjectResult(new[] { "Error 3" }) { StatusCode = StatusCodes.Status400BadRequest } - }; - yield return new object[] - { - new Failure("Error 4"), - new ObjectResult(new[] { "Error 4" }) { StatusCode = StatusCodes.Status400BadRequest } - }; - var cipher = new Cipher() { Id = Guid.NewGuid() }; - - yield return new object[] - { - new Success(cipher), - new ObjectResult(cipher) { StatusCode = StatusCodes.Status200OK } - }; - } - - - [Theory] - [MemberData(nameof(WithGenericTypeTestCases))] - public void MapToActionResult_WithGenericType_ShouldMapToHttpResponse(CommandResult input, ObjectResult expected) - { - var result = input.MapToActionResult(); - - Assert.Equivalent(expected, result); - } - - - [Fact] - public void MapToActionResult_WithGenericType_ShouldThrowExceptionForUnhandledCommandResult() - { - var result = new NotImplementedCommandResult(); - - Assert.Throws(() => result.MapToActionResult()); - } - - public static IEnumerable TestCases() - { - yield return new object[] - { - new NoRecordFoundFailure(new[] { "Error 1", "Error 2" }), - new ObjectResult(new[] { "Error 1", "Error 2" }) { StatusCode = StatusCodes.Status404NotFound } - }; - yield return new object[] - { - new BadRequestFailure("Error 3"), - new ObjectResult(new[] { "Error 3" }) { StatusCode = StatusCodes.Status400BadRequest } - }; - yield return new object[] - { - new Failure("Error 4"), - new ObjectResult(new[] { "Error 4" }) { StatusCode = StatusCodes.Status400BadRequest } - }; - yield return new object[] - { - new Success(), - new ObjectResult(new { }) { StatusCode = StatusCodes.Status200OK } - }; - } - - [Theory] - [MemberData(nameof(TestCases))] - public void MapToActionResult_ShouldMapToHttpResponse(CommandResult input, ObjectResult expected) - { - var result = input.MapToActionResult(); - - Assert.Equivalent(expected, result); - } - - [Fact] - public void MapToActionResult_ShouldThrowExceptionForUnhandledCommandResult() - { - var result = new NotImplementedCommandResult(); - - Assert.Throws(() => result.MapToActionResult()); - } -} - -public class NotImplementedCommandResult : CommandResult -{ - -} - -public class NotImplementedCommandResult : CommandResult -{ - -} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs index 80ce4cf481..e54e4aa99b 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs @@ -2,7 +2,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; -using Bit.Core.AdminConsole.Errors; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; @@ -11,12 +10,13 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.M using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Commands; +using Bit.Core.AdminConsole.Utilities.Errors; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; -using Bit.Core.Models.Commands; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.StaticStore; @@ -80,7 +80,7 @@ public class InviteOrganizationUserCommandTests // Assert Assert.IsType>(result); - Assert.Equal(NoUsersToInviteError.Code, (result as Failure).ErrorMessage); + Assert.Equal(NoUsersToInviteError.Code, (result as Failure)!.Error.Message); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() @@ -209,7 +209,7 @@ public class InviteOrganizationUserCommandTests Assert.IsType>(result); var failure = result as Failure; - Assert.Equal(errorMessage, failure!.ErrorMessage); + Assert.Equal(errorMessage, failure!.Error.Message); await sutProvider.GetDependency() .DidNotReceive() @@ -571,7 +571,7 @@ public class InviteOrganizationUserCommandTests // Assert Assert.IsType>(result); - Assert.Equal(FailedToInviteUsersError.Code, (result as Failure)!.ErrorMessage); + Assert.Equal(FailedToInviteUsersError.Code, (result as Failure)!.Error.Message); // org user revert await orgUserRepository.Received(1).DeleteManyAsync(Arg.Is>(x => x.Count() == 1)); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs index ee40fb1152..7c06e04256 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs @@ -2,7 +2,7 @@ using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Exceptions; using Bit.Core.Models.Business; @@ -156,6 +156,6 @@ public class InviteOrganizationUsersValidatorTests var result = await sutProvider.Sut.ValidateAsync(request); Assert.IsType>(result); - Assert.Equal("Some Secrets Manager Failure", (result as Invalid)!.ErrorMessageString); + Assert.Equal("Some Secrets Manager Failure", (result as Invalid)!.Error.Message); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs index 508b9f3cb0..be5586f8a6 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs @@ -1,7 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -36,7 +36,7 @@ public class InviteUserOrganizationValidationTests var result = await sutProvider.Sut.ValidateAsync(inviteOrganization); Assert.IsType>(result); - Assert.Equal(OrganizationNoPaymentMethodFoundError.Code, (result as Invalid)!.ErrorMessageString); + Assert.Equal(OrganizationNoPaymentMethodFoundError.Code, (result as Invalid)!.Error.Message); } [Theory] @@ -53,6 +53,6 @@ public class InviteUserOrganizationValidationTests var result = await sutProvider.Sut.ValidateAsync(inviteOrganization); Assert.IsType>(result); - Assert.Equal(OrganizationNoSubscriptionFoundError.Code, (result as Invalid)!.ErrorMessageString); + Assert.Equal(OrganizationNoSubscriptionFoundError.Code, (result as Invalid)!.Error.Message); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserPaymentValidationTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserPaymentValidationTests.cs index bcca89e1d2..d508f7cc5e 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserPaymentValidationTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserPaymentValidationTests.cs @@ -3,7 +3,7 @@ using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.StaticStore.Plans; @@ -39,7 +39,7 @@ public class InviteUserPaymentValidationTests }); Assert.IsType>(result); - Assert.Equal(PaymentCancelledSubscriptionError.Code, (result as Invalid)!.ErrorMessageString); + Assert.Equal(PaymentCancelledSubscriptionError.Code, (result as Invalid)!.Error.Message); } [Fact] diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs index c320ada8cb..571832d675 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs @@ -1,7 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Test.Common.AutoFixture; @@ -67,7 +67,7 @@ public class InviteUsersPasswordManagerValidatorTests var result = await sutProvider.Sut.ValidateAsync(subscriptionUpdate); Assert.IsType>(result); - Assert.Equal(PasswordManagerSeatLimitHasBeenReachedError.Code, (result as Invalid)!.ErrorMessageString); + Assert.Equal(PasswordManagerSeatLimitHasBeenReachedError.Code, (result as Invalid)!.Error.Message); } [Theory] @@ -88,6 +88,6 @@ public class InviteUsersPasswordManagerValidatorTests var result = await sutProvider.Sut.ValidateAsync(subscriptionUpdate); Assert.IsType>(result); - Assert.Equal(PasswordManagerPlanDoesNotAllowAdditionalSeatsError.Code, (result as Invalid)!.ErrorMessageString); + Assert.Equal(PasswordManagerPlanDoesNotAllowAdditionalSeatsError.Code, (result as Invalid)!.Error.Message); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs index d2809102aa..6048ed54d5 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs @@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; +using Bit.Core.AdminConsole.Utilities.Commands; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; @@ -11,7 +12,6 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Models.Commands; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs index 0edc2b5973..e368f77699 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs @@ -4,11 +4,11 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; +using Bit.Core.AdminConsole.Utilities.Commands; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Commands; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; diff --git a/test/Core.Test/AdminConsole/Shared/IValidatorTests.cs b/test/Core.Test/AdminConsole/Shared/IValidatorTests.cs index abb49c25c6..1bc673426d 100644 --- a/test/Core.Test/AdminConsole/Shared/IValidatorTests.cs +++ b/test/Core.Test/AdminConsole/Shared/IValidatorTests.cs @@ -1,5 +1,5 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Errors; +using Bit.Core.AdminConsole.Utilities.Validation; using Xunit; namespace Bit.Core.Test.AdminConsole.Shared; @@ -22,13 +22,11 @@ public class IValidatorTests { if (string.IsNullOrWhiteSpace(value.Name)) { - return Task.FromResult>(new Invalid - { - Errors = [new InvalidRequestError(value)] - }); + return Task.FromResult>( + new Invalid(new InvalidRequestError(value))); } - return Task.FromResult>(new Valid { Value = value }); + return Task.FromResult>(new Valid(value)); } } @@ -41,7 +39,7 @@ public class IValidatorTests Assert.IsType>(result); var invalidResult = result as Invalid; - Assert.Equal(InvalidRequestError.Code, invalidResult.Errors.First().Message); + Assert.Equal(InvalidRequestError.Code, invalidResult!.Error.Message); } [Fact] diff --git a/test/Core.Test/Models/Commands/CommandResultTests.cs b/test/Core.Test/AdminConsole/Utilities/Commands/CommandResultTests.cs similarity index 92% rename from test/Core.Test/Models/Commands/CommandResultTests.cs rename to test/Core.Test/AdminConsole/Utilities/Commands/CommandResultTests.cs index c500fef4f5..67ff59c95b 100644 --- a/test/Core.Test/Models/Commands/CommandResultTests.cs +++ b/test/Core.Test/AdminConsole/Utilities/Commands/CommandResultTests.cs @@ -1,9 +1,9 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.Models.Commands; +using Bit.Core.AdminConsole.Utilities.Commands; +using Bit.Core.AdminConsole.Utilities.Errors; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; -namespace Bit.Core.Test.Models.Commands; +namespace Bit.Core.Test.AdminConsole.Utilities.Commands; public class CommandResultTests { From 5f7e2b8a81c1b0f785e59322eee11357001c5cbd Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Fri, 9 May 2025 15:00:26 +0200 Subject: [PATCH 11/20] [PM-21075] Initial database seeder (#5703) Adds a database seeder which can be used standalone using a CLI for seeding your local development environment, or used in unit tests to seed complex scenarios. --------- Co-authored-by: Robert Y --- bitwarden-server.sln | 14 ++++++ ...nizationUsersControllerPerformanceTests.cs | 39 ++++++++++++++++ .../Api.IntegrationTest.csproj | 1 + util/DbSeederUtility/DbSeederUtility.csproj | 22 ++++++++++ util/DbSeederUtility/GlobalSettingsFactory.cs | 34 ++++++++++++++ util/DbSeederUtility/Program.cs | 39 ++++++++++++++++ util/DbSeederUtility/README.md | 40 +++++++++++++++++ .../ServiceCollectionExtension.cs | 25 +++++++++++ util/Seeder/Factories/OrganizationSeeder.cs | 44 +++++++++++++++++++ util/Seeder/Factories/UserSeeder.cs | 25 +++++++++++ util/Seeder/README.md | 18 ++++++++ .../Recipes/OrganizationWithUsersRecipe.cs | 37 ++++++++++++++++ util/Seeder/Seeder.csproj | 29 ++++++++++++ 13 files changed, 367 insertions(+) create mode 100644 test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs create mode 100644 util/DbSeederUtility/DbSeederUtility.csproj create mode 100644 util/DbSeederUtility/GlobalSettingsFactory.cs create mode 100644 util/DbSeederUtility/Program.cs create mode 100644 util/DbSeederUtility/README.md create mode 100644 util/DbSeederUtility/ServiceCollectionExtension.cs create mode 100644 util/Seeder/Factories/OrganizationSeeder.cs create mode 100644 util/Seeder/Factories/UserSeeder.cs create mode 100644 util/Seeder/README.md create mode 100644 util/Seeder/Recipes/OrganizationWithUsersRecipe.cs create mode 100644 util/Seeder/Seeder.csproj diff --git a/bitwarden-server.sln b/bitwarden-server.sln index 892d2f4255..2ec8d86e0e 100644 --- a/bitwarden-server.sln +++ b/bitwarden-server.sln @@ -129,6 +129,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.IntegrationTest", "t EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.IntegrationTest", "test\Core.IntegrationTest\Core.IntegrationTest.csproj", "{3631BA42-6731-4118-A917-DAA43C5032B9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seeder", "util\Seeder\Seeder.csproj", "{9A612EBA-1C0E-42B8-982B-62F0EE81000A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbSeederUtility\DbSeederUtility.csproj", "{17A89266-260A-4A03-81AE-C0468C6EE06E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -325,6 +329,14 @@ Global {3631BA42-6731-4118-A917-DAA43C5032B9}.Debug|Any CPU.Build.0 = Debug|Any CPU {3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.ActiveCfg = Release|Any CPU {3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.Build.0 = Release|Any CPU + {9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.Build.0 = Release|Any CPU + {17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -377,6 +389,8 @@ Global {4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} + {9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} + {17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs new file mode 100644 index 0000000000..94432b05a0 --- /dev/null +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs @@ -0,0 +1,39 @@ +using System.Net; +using System.Net.Http.Headers; +using Bit.Api.IntegrationTest.Factories; +using Bit.Seeder.Recipes; +using Xunit; +using Xunit.Abstractions; + +namespace Bit.Api.IntegrationTest.AdminConsole.Controllers; + +public class OrganizationUsersControllerPerformanceTest(ITestOutputHelper testOutputHelper) +{ + [Theory(Skip = "Performance test")] + [InlineData(100)] + [InlineData(60000)] + public async Task GetAsync(int seats) + { + await using var factory = new ApiApplicationFactory(); + var client = factory.CreateClient(); + + var db = factory.GetDatabaseContext(); + var seeder = new OrganizationWithUsersRecipe(db); + + var orgId = seeder.Seed("Org", seats, "large.test"); + + var tokens = await factory.LoginAsync("admin@large.test", "c55hlJ/cfdvTd4awTXUqow6X3cOQCfGwn11o3HblnPs="); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); + + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + + var response = await client.GetAsync($"/organizations/{orgId}/users?includeCollections=true"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var result = await response.Content.ReadAsStringAsync(); + Assert.NotEmpty(result); + + stopwatch.Stop(); + testOutputHelper.WriteLine($"Seed: {seats}; Request duration: {stopwatch.ElapsedMilliseconds} ms"); + } +} diff --git a/test/Api.IntegrationTest/Api.IntegrationTest.csproj b/test/Api.IntegrationTest/Api.IntegrationTest.csproj index 8fa74f98d4..a9d7fd502e 100644 --- a/test/Api.IntegrationTest/Api.IntegrationTest.csproj +++ b/test/Api.IntegrationTest/Api.IntegrationTest.csproj @@ -18,6 +18,7 @@ + diff --git a/util/DbSeederUtility/DbSeederUtility.csproj b/util/DbSeederUtility/DbSeederUtility.csproj new file mode 100644 index 0000000000..90ac7f22b4 --- /dev/null +++ b/util/DbSeederUtility/DbSeederUtility.csproj @@ -0,0 +1,22 @@ + + + + Exe + net8.0 + enable + enable + Bit.DbSeederUtility + DbSeeder + true + 2294c6ba-7cd0-4293-a797-3882e41c61cb + + + + + + + + + + + diff --git a/util/DbSeederUtility/GlobalSettingsFactory.cs b/util/DbSeederUtility/GlobalSettingsFactory.cs new file mode 100644 index 0000000000..e4ad275a0e --- /dev/null +++ b/util/DbSeederUtility/GlobalSettingsFactory.cs @@ -0,0 +1,34 @@ +using Bit.Core.Settings; +using Microsoft.Extensions.Configuration; + +namespace Bit.DbSeederUtility; + +public static class GlobalSettingsFactory +{ + private static GlobalSettings? _globalSettings; + + public static GlobalSettings GlobalSettings + { + get { return _globalSettings ??= LoadGlobalSettings(); } + } + + private static GlobalSettings LoadGlobalSettings() + { + Console.WriteLine("Loading global settings..."); + + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true, reloadOnChange: true) + .AddUserSecrets("bitwarden-Api") // Load user secrets from the API project + .AddEnvironmentVariables(); + + var configuration = configBuilder.Build(); + var globalSettingsSection = configuration.GetSection("globalSettings"); + + var settings = new GlobalSettings(); + globalSettingsSection.Bind(settings); + + return settings; + } +} diff --git a/util/DbSeederUtility/Program.cs b/util/DbSeederUtility/Program.cs new file mode 100644 index 0000000000..2d75b31934 --- /dev/null +++ b/util/DbSeederUtility/Program.cs @@ -0,0 +1,39 @@ +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Seeder.Recipes; +using CommandDotNet; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.DbSeederUtility; + +public class Program +{ + private static int Main(string[] args) + { + return new AppRunner() + .Run(args); + } + + [Command("organization", Description = "Seed an organization and organization users")] + public void Organization( + [Option('n', "Name", Description = "Name of organization")] + string name, + [Option('u', "users", Description = "Number of users to generate")] + int users, + [Option('d', "domain", Description = "Email domain for users")] + string domain + ) + { + // Create service provider with necessary services + var services = new ServiceCollection(); + ServiceCollectionExtension.ConfigureServices(services); + var serviceProvider = services.BuildServiceProvider(); + + // Get a scoped DB context + using var scope = serviceProvider.CreateScope(); + var scopedServices = scope.ServiceProvider; + var db = scopedServices.GetRequiredService(); + + var recipe = new OrganizationWithUsersRecipe(db); + recipe.Seed(name, users, domain); + } +} diff --git a/util/DbSeederUtility/README.md b/util/DbSeederUtility/README.md new file mode 100644 index 0000000000..0eb21ae6c5 --- /dev/null +++ b/util/DbSeederUtility/README.md @@ -0,0 +1,40 @@ +# Bitwarden Database Seeder Utility + +A command-line utility for generating and managing test data for Bitwarden databases. + +## Overview + +DbSeederUtility is an executable wrapper around the Seeder class library that provides a convenient command-line +interface for executing seed-recipes in your local environment. + +## Installation + +The utility can be built and run as a .NET 8 application: + +``` +dotnet build +dotnet run -- [options] +``` + +Or directly using the compiled executable: + +``` +DbSeeder.exe [options] +``` + +## Examples + +### Generate and load test organization + +```bash +# Generate an organization called "seeded" with 10000 users using the @large.test email domain. +# Login using "admin@large.test" with password "asdfasdfasdf" +DbSeeder.exe organization -n seeded -u 10000 -d large.test +``` + +## Dependencies + +This utility depends on: +- The Seeder class library +- CommandDotNet for command-line parsing +- .NET 8.0 runtime diff --git a/util/DbSeederUtility/ServiceCollectionExtension.cs b/util/DbSeederUtility/ServiceCollectionExtension.cs new file mode 100644 index 0000000000..0653bb1801 --- /dev/null +++ b/util/DbSeederUtility/ServiceCollectionExtension.cs @@ -0,0 +1,25 @@ +using Bit.SharedWeb.Utilities; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Bit.DbSeederUtility; + +public static class ServiceCollectionExtension +{ + public static void ConfigureServices(ServiceCollection services) + { + // Load configuration using the GlobalSettingsFactory + var globalSettings = GlobalSettingsFactory.GlobalSettings; + + // Register services + services.AddLogging(builder => builder.AddConsole()); + services.AddSingleton(globalSettings); + + // Add Data Protection services + services.AddDataProtection() + .SetApplicationName("Bitwarden"); + + services.AddDatabaseRepositories(globalSettings); + } +} diff --git a/util/Seeder/Factories/OrganizationSeeder.cs b/util/Seeder/Factories/OrganizationSeeder.cs new file mode 100644 index 0000000000..5e5cb17419 --- /dev/null +++ b/util/Seeder/Factories/OrganizationSeeder.cs @@ -0,0 +1,44 @@ +using Bit.Core.Billing.Enums; +using Bit.Core.Enums; +using Bit.Infrastructure.EntityFramework.AdminConsole.Models; +using Bit.Infrastructure.EntityFramework.Models; + +namespace Bit.Seeder.Factories; + +public class OrganizationSeeder +{ + public static Organization CreateEnterprise(string name, string domain, int seats) + { + return new Organization + { + Id = Guid.NewGuid(), + Name = name, + BillingEmail = $"billing@{domain}", + Plan = "Enterprise (Annually)", + PlanType = PlanType.EnterpriseAnnually, + Seats = seats, + + // Currently hardcoded to the values from https://github.com/bitwarden/sdk-internal/blob/main/crates/bitwarden-core/src/client/test_accounts.rs. + // TODO: These should be dynamically generated by the SDK. + PublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmIJbGMk6eZqVE7UxhZ46Weu2jKciqOiOkSVYtGvs61rfe9AXxtLaaZEKN4d4DmkZcF6dna2eXNxZmb7U4pwlttye8ksqISe6IUAZQox7auBpjopdCEPhKRg3BD/u8ks9UxSxgWe+fpebjt6gd5hsl1/5HOObn7SeU6EEU04cp3/eH7a4OTdXxB8oN62HGV9kM/ubM1goILgjoSJDbihMK0eb7b8hPHwcA/YOgKKiu/N3FighccdSMD5Pk+HfjacsFNZQa2EsqW09IvvSZ+iL6HQeZ1vwc/6TO1J7EOfJZFQcjoEL9LVI693efYoMZSmrPEWziZ4PvwpOOGo6OObyMQIDAQAB", + PrivateKey = "2.6FggyKVyaKQsfohi5yqgbg==|UU2JeafOB41L5UscGmf4kq15JGDf3Bkf67KECiehTODzbWctVLTgyDk0Qco8/6CMN6nZGXjxR2A4r5ExhmwRNsNxd77G+MprkmiJz+7w33ROZ1ouQO5XjD3wbQ3ssqNiTKId6yAUPBvuAZRixVApauTuADc8QWGixqCQcqZzmU7YSBBIPf652/AEYr4Tk64YihoE39pHiK8MRbTLdRt3EF4LSMugPAPM24vCgUv3w1TD3Fj6sDg/6oi3flOV9SJZX4vCiUXbDNEuD/p2aQrEXVbaxweFOHjTe7F4iawjXw3nG3SO8rUBHcxbhDDVx5rjYactbW5QvHWiyla6uLb6o8WHBneg2EjTEwAHOZE/rBjcqmAJb2sVp1E0Kwq8ycGmL69vmqJPC1GqVTohAQvmEkaxIPpfq24Yb9ZPrADA7iEXBKuAQ1FphFUVgJBJGJbd60sOV1Rz1T+gUwS4wCNQ4l3LG1S22+wzUVlEku5DXFnT932tatqTyWEthqPqLCt6dL1+qa94XLpeHagXAx2VGe8n8IlcADtxqS+l8xQ4heT12WO9kC316vqvg1mnsI56faup9hb3eT9ZpKyxSBGYOphlTWfV1Y/v64f5PYvTo4aL0IYHyLY/9Qi72vFmOpPeHBYgD5t3j+H2CsiU1PkYsBggOmD7xW8FDuT6HWVvwhEJqeibVPK0Lhyj6tgvlSIAvFUaSMFPlmwFNmwfj/AHUhr9KuTfsBFTZ10yy9TZVgf+EofwnrxHBaWUgdD40aHoY1VjfG33iEuajb6buxG3pYFyPNhJNzeLZisUKIDRMQpUHrsE22EyrFFran3tZGdtcyIEK4Q1F0ULYzJ6T9iY25/ZgPy3pEAAMZCtqo3s+GjX295fWIHfMcnjMgNUHPjExjWBHa+ggK9iQXkFpBVyYB1ga/+0eiIhiek3PlgtvpDrqF7TsLK+ROiBw2GJ7uaO3EEXOj2GpNBuEJ5CdodhZkwzhwMcSatgDHkUuNVu0iVbF6/MxVdOxWXKO+jCYM6PZk/vAhLYqpPzu2T2Uyz4nkDs2Tiq61ez6FoCrzdHIiyIxVTzUQH8G9FgSmtaZ7GCbqlhnurYgcMciwPzxg0hpAQT+NZw1tVEii9vFSpJJbGJqNhORKfKh/Mu1P/9LOQq7Y0P2FIR3x/eUVEQ7CGv2jVtO5ryGSmKeq/P9Fr54wTPaNiqN2K+leACUznCdUWw8kZo/AsBcrOe4OkRX6k8LC3oeJXy06DEToatxEvPYemUauhxiXRw8nfNMqc4LyJq2bbT0zCgJHoqpozPdNg6AYWcoIobgAGu7ZQGq+oE1MT3GZxotMPe/NUJiAc5YE9Thb5Yf3gyno71pyqPTVl/6IQuh4SUz7rkgwF/aVHEnr4aUYNoc0PEzd2Me0jElsA3GAneq1I/wngutOWgTViTK4Nptr5uIzMVQs9H1rOMJNorP8b02t1NDu010rSsib9GaaJJq4r4iy46laQOxWoU0ex26arYnk+jw4833WSCTVBIprTgizZ+fKjoY0xwXvI2oOvGNEUCtGFvKFORTaQrlaXZIg1toa2BBVNicyONbwnI3KIu3MgGJ2SlCVXJn8oHFppVHFCdwgN1uDzGiKAhjvr0sZTUtXin2f2CszPTbbo=|fUhbVKrr8CSKE7TZJneXpDGraj5YhRrq9ESo206S+BY=", + }; + } +} + +public static class OrgnaizationExtensions +{ + public static OrganizationUser CreateOrganizationUser(this Organization organization, User user) + { + return new OrganizationUser + { + Id = Guid.NewGuid(), + OrganizationId = organization.Id, + UserId = user.Id, + + Key = "4.rY01mZFXHOsBAg5Fq4gyXuklWfm6mQASm42DJpx05a+e2mmp+P5W6r54WU2hlREX0uoTxyP91bKKwickSPdCQQ58J45LXHdr9t2uzOYyjVzpzebFcdMw1eElR9W2DW8wEk9+mvtWvKwu7yTebzND+46y1nRMoFydi5zPVLSlJEf81qZZ4Uh1UUMLwXz+NRWfixnGXgq2wRq1bH0n3mqDhayiG4LJKgGdDjWXC8W8MMXDYx24SIJrJu9KiNEMprJE+XVF9nQVNijNAjlWBqkDpsfaWTUfeVLRLctfAqW1blsmIv4RQ91PupYJZDNc8nO9ZTF3TEVM+2KHoxzDJrLs2Q==", + Type = OrganizationUserType.Admin, + Status = OrganizationUserStatusType.Confirmed + }; + } +} diff --git a/util/Seeder/Factories/UserSeeder.cs b/util/Seeder/Factories/UserSeeder.cs new file mode 100644 index 0000000000..90cadf0b78 --- /dev/null +++ b/util/Seeder/Factories/UserSeeder.cs @@ -0,0 +1,25 @@ +using Bit.Core.Enums; +using Bit.Infrastructure.EntityFramework.Models; + +namespace Bit.Seeder.Factories; + +public class UserSeeder +{ + public static User CreateUser(string email) + { + return new User + { + Id = Guid.NewGuid(), + Email = email, + MasterPassword = "AQAAAAIAAYagAAAAEBATmF66OHMpHuHKc1CsGZQ1ltHUHyhYK+7e4re3bVFi16SOpLpDfzdFswnvFQs2Rg==", + SecurityStamp = "4830e359-e150-4eae-be2a-996c81c5e609", + Key = "2.z/eLKFhd62qy9RzXu3UHgA==|fF6yNupiCIguFKSDTB3DoqcGR0Xu4j+9VlnMyT5F3PaWIcGhzQKIzxdB95nhslaCQv3c63M7LBnvzVo1J9SUN85RMbP/57bP1HvhhU1nvL8=|IQPtf8v7k83MFZEhazSYXSdu98BBU5rqtvC4keVWyHM=", + PublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Ww2chogqCpaAR7Uw448am4b7vDFXiM5kXjFlGfXBlrAdAqTTggEvTDlMNYqPlCo+mBM6iFmTTUY9rpZBvFskMnKvsvpJ47/fehAH2o2e3Ulv/5NFevaVCMCmpkBDtbMbO1A4a3btdRtCP8DsKWMefHauEpaoLxNTLWnOIZVfCMjsSgx2EvULHAZPTtbFwm4+UVKniM4ds4jvOsD85h4jn2aLs/jWJXFfxN8iVSqEqpC2TBvsPdyHb49xQoWWfF0Z6BiNqeNGKEU9Uos1pjL+kzhEzzSpH31PZT/ufJ/oo4+93wrUt57hb6f0jxiXhwd5yQ+9F6wVwpbfkq0IwhjOwIDAQAB", + PrivateKey = "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=", + ApiKey = "7gp59kKHt9kMlks0BuNC4IjNXYkljR", + + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 600_000, + }; + } +} diff --git a/util/Seeder/README.md b/util/Seeder/README.md new file mode 100644 index 0000000000..8597ad6e39 --- /dev/null +++ b/util/Seeder/README.md @@ -0,0 +1,18 @@ +# Bitwarden Database Seeder + +A class library for generating and inserting test data. + +## Project Structure + +The project is organized into these main components: + +### Factories + +Factories are helper classes for creating domain entities and populating them with realistic data. This assist in +decreasing the amount of boilerplate code needed to create test data in recipes. + +### Recipes + +Recipes are pre-defined data sets which can be run to generate and load data into the database. They often allow a allow +for a few arguments to customize the data slightly. Recipes should be kept simple and focused on a single task. Default +to creating more recipes rather than adding complexity to existing ones. diff --git a/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs b/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs new file mode 100644 index 0000000000..fb06c091ae --- /dev/null +++ b/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs @@ -0,0 +1,37 @@ +using Bit.Infrastructure.EntityFramework.Models; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Seeder.Factories; +using LinqToDB.EntityFrameworkCore; + +namespace Bit.Seeder.Recipes; + +public class OrganizationWithUsersRecipe(DatabaseContext db) +{ + public Guid Seed(string name, int users, string domain) + { + var organization = OrganizationSeeder.CreateEnterprise(name, domain, users); + var user = UserSeeder.CreateUser($"admin@{domain}"); + var orgUser = organization.CreateOrganizationUser(user); + + var additionalUsers = new List(); + var additionalOrgUsers = new List(); + for (var i = 0; i < users; i++) + { + var additionalUser = UserSeeder.CreateUser($"user{i}@{domain}"); + additionalUsers.Add(additionalUser); + additionalOrgUsers.Add(organization.CreateOrganizationUser(additionalUser)); + } + + db.Add(organization); + db.Add(user); + db.Add(orgUser); + + db.SaveChanges(); + + // Use LinqToDB's BulkCopy for significant better performance + db.BulkCopy(additionalUsers); + db.BulkCopy(additionalOrgUsers); + + return organization.Id; + } +} diff --git a/util/Seeder/Seeder.csproj b/util/Seeder/Seeder.csproj new file mode 100644 index 0000000000..392f6434cc --- /dev/null +++ b/util/Seeder/Seeder.csproj @@ -0,0 +1,29 @@ + + + + + net8.0 + enable + enable + Bit.Seeder + Bit.Seeder + Core library for generating and managing test data for Bitwarden + library + false + + + + + + + + + + + + + + + + + From 3989e3b26bb254f86eafd13e552ce3acc4192069 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Fri, 9 May 2025 09:37:16 -0400 Subject: [PATCH 12/20] chore(feature-flag): [PM-8671] Remove new-device-verification feature flag * Completed grouping of feature flags by team. * Completed grouping feature flags by team. * Remove email delay feature flag * Removed feature flag * Fixed reference. * Remove flag after merge. * Removed flag from server. * Removed feature flag from server * Remove new device verification feature flag. * Removed unnecessary using. * Remove feature flag from Constants --- src/Admin/Controllers/UsersController.cs | 1 - .../Auth/Controllers/AccountsController.cs | 1 - src/Core/Constants.cs | 1 - .../RequestValidators/DeviceValidator.cs | 8 ++---- .../IdentityServer/DeviceValidatorTests.cs | 25 +++---------------- 5 files changed, 5 insertions(+), 31 deletions(-) diff --git a/src/Admin/Controllers/UsersController.cs b/src/Admin/Controllers/UsersController.cs index 71be19a041..cecd7a2142 100644 --- a/src/Admin/Controllers/UsersController.cs +++ b/src/Admin/Controllers/UsersController.cs @@ -167,7 +167,6 @@ public class UsersController : Controller [HttpPost] [ValidateAntiForgeryToken] [RequirePermission(Permission.User_NewDeviceException_Edit)] - [RequireFeature(FeatureFlagKeys.NewDeviceVerification)] public async Task ToggleNewDeviceVerification(Guid id) { var user = await _userRepository.GetByIdAsync(id); diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 621524228a..2134a7fc4e 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -693,7 +693,6 @@ public class AccountsController : Controller } } - [RequireFeature(FeatureFlagKeys.NewDeviceVerification)] [AllowAnonymous] [HttpPost("resend-new-device-otp")] public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index a27738fd19..3399a729d1 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -115,7 +115,6 @@ public static class FeatureFlagKeys public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; public const string EmailVerification = "email-verification"; public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh"; - public const string NewDeviceVerification = "new-device-verification"; public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor"; public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor"; public const string RecoveryCodeLogin = "pm-17128-recovery-code-login"; diff --git a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs index 36a08326ab..4dc77c4449 100644 --- a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs @@ -22,8 +22,7 @@ public class DeviceValidator( ICurrentContext currentContext, IUserService userService, IDistributedCache distributedCache, - ILogger logger, - IFeatureService featureService) : IDeviceValidator + ILogger logger) : IDeviceValidator { private readonly IDeviceService _deviceService = deviceService; private readonly IDeviceRepository _deviceRepository = deviceRepository; @@ -33,7 +32,6 @@ public class DeviceValidator( private readonly IUserService _userService = userService; private readonly IDistributedCache distributedCache = distributedCache; private readonly ILogger _logger = logger; - private readonly IFeatureService _featureService = featureService; public async Task ValidateRequestDeviceAsync(ValidatedTokenRequest request, CustomValidatorRequestContext context) { @@ -64,9 +62,7 @@ public class DeviceValidator( } // We have established that the device is unknown at this point; begin new device verification - // PM-13340: remove feature flag - if (_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) && - request.GrantType == "password" && + if (request.GrantType == "password" && request.Raw["AuthRequest"] == null && !context.TwoFactorRequired && !context.SsoRequired && diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs index b71dd6c230..9e20e630cd 100644 --- a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -1,5 +1,4 @@ -using Bit.Core; -using Bit.Core.Context; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Api; @@ -28,7 +27,7 @@ public class DeviceValidatorTests private readonly IUserService _userService; private readonly IDistributedCache _distributedCache; private readonly Logger _logger; - private readonly IFeatureService _featureService; + private readonly DeviceValidator _sut; public DeviceValidatorTests() @@ -41,7 +40,6 @@ public class DeviceValidatorTests _userService = Substitute.For(); _distributedCache = Substitute.For(); _logger = new Logger(Substitute.For()); - _featureService = Substitute.For(); _sut = new DeviceValidator( _deviceService, _deviceRepository, @@ -50,8 +48,7 @@ public class DeviceValidatorTests _currentContext, _userService, _distributedCache, - _logger, - _featureService); + _logger); } [Theory, BitAutoData] @@ -312,8 +309,6 @@ public class DeviceValidatorTests AddValidDeviceToRequest(request); _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) .Returns(null as Device); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) - .Returns(true); request.GrantType = grantType; @@ -336,8 +331,6 @@ public class DeviceValidatorTests AddValidDeviceToRequest(request); _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) .Returns(null as Device); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) - .Returns(true); request.Raw.Add("AuthRequest", "authRequest"); @@ -360,8 +353,6 @@ public class DeviceValidatorTests AddValidDeviceToRequest(request); _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) .Returns(null as Device); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) - .Returns(true); context.TwoFactorRequired = true; @@ -384,8 +375,6 @@ public class DeviceValidatorTests AddValidDeviceToRequest(request); _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) .Returns(null as Device); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) - .Returns(true); context.SsoRequired = true; @@ -404,7 +393,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; context.User = null; @@ -430,7 +418,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; context.User.VerifyDevices = false; @@ -454,7 +441,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _distributedCache.GetAsync(Arg.Any()).Returns(null as byte[]); context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromHours(23); @@ -479,7 +465,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _distributedCache.GetAsync(Arg.Any()).Returns([1]); @@ -503,7 +488,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _distributedCache.GetAsync(Arg.Any()).Returns(null as byte[]); @@ -535,7 +519,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _distributedCache.GetAsync(Arg.Any()).Returns(null as byte[]); @@ -564,7 +547,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _distributedCache.GetAsync(Arg.Any()).Returns([1]); _deviceRepository.GetManyByUserIdAsync(context.User.Id).Returns([]); @@ -590,7 +572,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _deviceRepository.GetManyByUserIdAsync(context.User.Id).Returns([new Device()]); _distributedCache.GetAsync(Arg.Any()).Returns(null as byte[]); From 2918d46b62c674dfd9cfd631f3995c70ae25ebc2 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 9 May 2025 10:12:43 -0400 Subject: [PATCH 13/20] Resolve Vault warnings (#5786) - Also remove extra exclusions --- src/Core/Core.csproj | 2 +- .../Vault/Queries/GetCipherPermissionsForUserQuery.cs | 4 ++-- src/Core/Vault/Repositories/ICipherRepository.cs | 1 + src/Infrastructure.Dapper/Infrastructure.Dapper.csproj | 5 ----- .../Infrastructure.EntityFramework.csproj | 2 +- test/Api.Test/Api.Test.csproj | 2 -- test/Core.Test/Core.Test.csproj | 2 -- .../Vault/Queries/GetTasksForOrganizationQueryTests.cs | 8 ++++---- 8 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index ba48b6175b..4411a3de9b 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -4,7 +4,7 @@ false bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml - $(WarningsNotAsErrors);CS1574;CS9113;CS1998 + $(WarningsNotAsErrors);CS1574;CS9113 diff --git a/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs b/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs index 5cce87e958..07e9d07299 100644 --- a/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs +++ b/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs @@ -45,7 +45,7 @@ public class GetCipherPermissionsForUserQuery : IGetCipherPermissionsForUserQuer cipher.Value.ViewPassword = true; } } - else if (await CanAccessUnassignedCiphersAsync(org)) + else if (CanAccessUnassignedCiphers(org)) { var unassignedCiphers = await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organizationId); foreach (var unassignedCipher in unassignedCiphers) @@ -83,7 +83,7 @@ public class GetCipherPermissionsForUserQuery : IGetCipherPermissionsForUserQuer return false; } - private async Task CanAccessUnassignedCiphersAsync(CurrentContextOrganization org) + private bool CanAccessUnassignedCiphers(CurrentContextOrganization org) { if (org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or diff --git a/src/Core/Vault/Repositories/ICipherRepository.cs b/src/Core/Vault/Repositories/ICipherRepository.cs index b094b42044..f6767fada2 100644 --- a/src/Core/Vault/Repositories/ICipherRepository.cs +++ b/src/Core/Vault/Repositories/ICipherRepository.cs @@ -3,6 +3,7 @@ using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Queries; namespace Bit.Core.Vault.Repositories; diff --git a/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj b/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj index b26dc938cf..8feb455feb 100644 --- a/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj +++ b/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj @@ -1,10 +1,5 @@ - - - $(WarningsNotAsErrors);CS8618 - - diff --git a/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj b/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj index a11a209b39..639d88524b 100644 --- a/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj +++ b/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj @@ -2,7 +2,7 @@ - $(WarningsNotAsErrors);CS0108;CS8632 + $(WarningsNotAsErrors);CS0108 diff --git a/test/Api.Test/Api.Test.csproj b/test/Api.Test/Api.Test.csproj index ec22583caf..d6b31ce930 100644 --- a/test/Api.Test/Api.Test.csproj +++ b/test/Api.Test/Api.Test.csproj @@ -2,8 +2,6 @@ false - - $(WarningsNotAsErrors);CS8620;CS0169 diff --git a/test/Core.Test/Core.Test.csproj b/test/Core.Test/Core.Test.csproj index cc19c50c35..c0f91a7bd3 100644 --- a/test/Core.Test/Core.Test.csproj +++ b/test/Core.Test/Core.Test.csproj @@ -2,8 +2,6 @@ false Bit.Core.Test - - $(WarningsNotAsErrors);CS4014 diff --git a/test/Core.Test/Vault/Queries/GetTasksForOrganizationQueryTests.cs b/test/Core.Test/Vault/Queries/GetTasksForOrganizationQueryTests.cs index 59ec7350da..f72a1f5f82 100644 --- a/test/Core.Test/Vault/Queries/GetTasksForOrganizationQueryTests.cs +++ b/test/Core.Test/Vault/Queries/GetTasksForOrganizationQueryTests.cs @@ -40,12 +40,12 @@ public class GetTasksForOrganizationQueryTests var result = await sutProvider.Sut.GetTasksAsync(org.Id, status); Assert.Equal(2, result.Count); - sutProvider.GetDependency().Received(1).AuthorizeAsync( + await sutProvider.GetDependency().Received(1).AuthorizeAsync( Arg.Any(), org, Arg.Is>( e => e.Contains(SecurityTaskOperations.ListAllForOrganization) ) ); - sutProvider.GetDependency().Received(1).GetManyByOrganizationIdStatusAsync(org.Id, SecurityTaskStatus.Pending); + await sutProvider.GetDependency().Received(1).GetManyByOrganizationIdStatusAsync(org.Id, SecurityTaskStatus.Pending); } [Theory, BitAutoData] @@ -82,11 +82,11 @@ public class GetTasksForOrganizationQueryTests await Assert.ThrowsAsync(() => sutProvider.Sut.GetTasksAsync(org.Id)); - sutProvider.GetDependency().Received(1).AuthorizeAsync( + await sutProvider.GetDependency().Received(1).AuthorizeAsync( Arg.Any(), org, Arg.Is>( e => e.Contains(SecurityTaskOperations.ListAllForOrganization) ) ); - sutProvider.GetDependency().Received(0).GetManyByOrganizationIdStatusAsync(org.Id, SecurityTaskStatus.Pending); + await sutProvider.GetDependency().Received(0).GetManyByOrganizationIdStatusAsync(org.Id, SecurityTaskStatus.Pending); } } From 80e7a0afd60da516481731fe4a9a3c1145aff6bc Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Fri, 9 May 2025 10:44:38 -0400 Subject: [PATCH 14/20] chore(captcha): [PM-15162] Remove captcha enforcement and issuing of bypass token * Remove captcha enforcement and issuing/verification of bypass token * Removed more captcha logic. * Removed logic to enforce failed login attempts * Linting. * Fixed order of initialization. * Fixed merge conflicts * Renamed registration finish response for clarity * Remove unnecessary mailService references. --- .../Api/Request/ICaptchaProtectedModel.cs | 6 - .../Auth/Models/Business/CaptchaResponse.cs | 9 -- .../Business/Tokenables/HCaptchaTokenable.cs | 43 ------ .../Services/ICaptchaValidationService.cs | 15 -- .../HCaptchaValidationService.cs | 132 ------------------ .../NoopCaptchaValidationService.cs | 18 --- .../Utilities/CaptchaProtectedAttribute.cs | 36 ----- .../Auth/FailedLoginAttempts.html.hbs | 31 ---- .../Auth/FailedLoginAttempts.text.hbs | 13 -- .../Auth/FailedTwoFactorAttempts.html.hbs | 31 ---- .../Auth/FailedTwoFactorAttempts.text.hbs | 13 -- src/Core/Services/IMailService.cs | 2 - .../Implementations/HandlebarsMailService.cs | 34 ----- .../NoopImplementations/NoopMailService.cs | 10 -- src/Core/Settings/GlobalSettings.cs | 11 -- .../Controllers/AccountsController.cs | 11 +- .../CustomValidatorRequestContext.cs | 6 +- .../RequestValidators/BaseRequestValidator.cs | 50 +------ .../CustomTokenRequestValidator.cs | 2 - .../ResourceOwnerPasswordValidator.cs | 36 ----- .../WebAuthnGrantValidator.cs | 2 - .../Request/Accounts/RegisterRequestModel.cs | 4 +- .../ICaptchaProtectedResponseModel.cs | 5 - .../Accounts/RegisterFinishResponseModel.cs | 10 ++ .../Accounts/RegisterResponseModel.cs | 14 -- src/Identity/appsettings.Production.json | 3 - src/Identity/appsettings.SelfHosted.json | 3 - .../Utilities/ServiceCollectionExtensions.cs | 18 --- .../Tokenables/HCaptchaTokenableTests.cs | 87 ------------ .../Business/Tokenables/SsoTokenableTests.cs | 2 +- .../Controllers/AccountsControllerTests.cs | 4 - .../BaseRequestValidatorTests.cs | 81 ----------- .../BaseRequestValidatorTestWrapper.cs | 2 - .../Factories/WebApplicationFactoryBase.cs | 3 - util/Setup/Configuration.cs | 3 - util/Setup/NginxConfigBuilder.cs | 2 - util/Setup/Templates/NginxConfig.hbs | 10 -- 37 files changed, 22 insertions(+), 740 deletions(-) delete mode 100644 src/Core/Auth/Models/Api/Request/ICaptchaProtectedModel.cs delete mode 100644 src/Core/Auth/Models/Business/CaptchaResponse.cs delete mode 100644 src/Core/Auth/Models/Business/Tokenables/HCaptchaTokenable.cs delete mode 100644 src/Core/Auth/Services/ICaptchaValidationService.cs delete mode 100644 src/Core/Auth/Services/Implementations/HCaptchaValidationService.cs delete mode 100644 src/Core/Auth/Services/NoopImplementations/NoopCaptchaValidationService.cs delete mode 100644 src/Core/Auth/Utilities/CaptchaProtectedAttribute.cs delete mode 100644 src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.html.hbs delete mode 100644 src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.text.hbs delete mode 100644 src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.html.hbs delete mode 100644 src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.text.hbs delete mode 100644 src/Identity/Models/Response/Accounts/ICaptchaProtectedResponseModel.cs create mode 100644 src/Identity/Models/Response/Accounts/RegisterFinishResponseModel.cs delete mode 100644 src/Identity/Models/Response/Accounts/RegisterResponseModel.cs delete mode 100644 test/Core.Test/Auth/Models/Business/Tokenables/HCaptchaTokenableTests.cs diff --git a/src/Core/Auth/Models/Api/Request/ICaptchaProtectedModel.cs b/src/Core/Auth/Models/Api/Request/ICaptchaProtectedModel.cs deleted file mode 100644 index 6968a904b0..0000000000 --- a/src/Core/Auth/Models/Api/Request/ICaptchaProtectedModel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Bit.Core.Auth.Models.Api; - -public interface ICaptchaProtectedModel -{ - string CaptchaResponse { get; set; } -} diff --git a/src/Core/Auth/Models/Business/CaptchaResponse.cs b/src/Core/Auth/Models/Business/CaptchaResponse.cs deleted file mode 100644 index 1a4b039ec0..0000000000 --- a/src/Core/Auth/Models/Business/CaptchaResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Bit.Core.Auth.Models.Business; - -public class CaptchaResponse -{ - public bool Success { get; set; } - public bool MaybeBot { get; set; } - public bool IsBot { get; set; } - public double Score { get; set; } -} diff --git a/src/Core/Auth/Models/Business/Tokenables/HCaptchaTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/HCaptchaTokenable.cs deleted file mode 100644 index 72994563c1..0000000000 --- a/src/Core/Auth/Models/Business/Tokenables/HCaptchaTokenable.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Text.Json.Serialization; -using Bit.Core.Entities; -using Bit.Core.Tokens; - -namespace Bit.Core.Auth.Models.Business.Tokenables; - -public class HCaptchaTokenable : ExpiringTokenable -{ - private const double _tokenLifetimeInHours = (double)5 / 60; // 5 minutes - public const string ClearTextPrefix = "BWCaptchaBypass_"; - public const string DataProtectorPurpose = "CaptchaServiceDataProtector"; - public const string TokenIdentifier = "CaptchaBypassToken"; - - public string Identifier { get; set; } = TokenIdentifier; - public Guid Id { get; set; } - public string Email { get; set; } - - [JsonConstructor] - public HCaptchaTokenable() - { - ExpirationDate = DateTime.UtcNow.AddHours(_tokenLifetimeInHours); - } - - public HCaptchaTokenable(User user) : this() - { - Id = user?.Id ?? default; - Email = user?.Email; - } - - public bool TokenIsValid(User user) - { - if (Id == default || Email == default || user == null) - { - return false; - } - - return Id == user.Id && - Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase); - } - - // Validates deserialized - protected override bool TokenIsValid() => Identifier == TokenIdentifier && Id != default && !string.IsNullOrWhiteSpace(Email); -} diff --git a/src/Core/Auth/Services/ICaptchaValidationService.cs b/src/Core/Auth/Services/ICaptchaValidationService.cs deleted file mode 100644 index 8547c68f7a..0000000000 --- a/src/Core/Auth/Services/ICaptchaValidationService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Bit.Core.Auth.Models.Business; -using Bit.Core.Context; -using Bit.Core.Entities; - -namespace Bit.Core.Auth.Services; - -public interface ICaptchaValidationService -{ - string SiteKey { get; } - string SiteKeyResponseKeyName { get; } - bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null); - Task ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress, - User user = null); - string GenerateCaptchaBypassToken(User user); -} diff --git a/src/Core/Auth/Services/Implementations/HCaptchaValidationService.cs b/src/Core/Auth/Services/Implementations/HCaptchaValidationService.cs deleted file mode 100644 index cdd6c2017e..0000000000 --- a/src/Core/Auth/Services/Implementations/HCaptchaValidationService.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System.Net.Http.Json; -using System.Text.Json.Serialization; -using Bit.Core.Auth.Models.Business; -using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Context; -using Bit.Core.Entities; -using Bit.Core.Settings; -using Bit.Core.Tokens; -using Microsoft.Extensions.Logging; - -namespace Bit.Core.Auth.Services; - -public class HCaptchaValidationService : ICaptchaValidationService -{ - private readonly ILogger _logger; - private readonly IHttpClientFactory _httpClientFactory; - private readonly GlobalSettings _globalSettings; - private readonly IDataProtectorTokenFactory _tokenizer; - - public HCaptchaValidationService( - ILogger logger, - IHttpClientFactory httpClientFactory, - IDataProtectorTokenFactory tokenizer, - GlobalSettings globalSettings) - { - _logger = logger; - _httpClientFactory = httpClientFactory; - _globalSettings = globalSettings; - _tokenizer = tokenizer; - } - - public string SiteKeyResponseKeyName => "HCaptcha_SiteKey"; - public string SiteKey => _globalSettings.Captcha.HCaptchaSiteKey; - - public string GenerateCaptchaBypassToken(User user) => _tokenizer.Protect(new HCaptchaTokenable(user)); - - public async Task ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress, - User user = null) - { - var response = new CaptchaResponse { Success = false }; - if (string.IsNullOrWhiteSpace(captchaResponse)) - { - return response; - } - - if (user != null && ValidateCaptchaBypassToken(captchaResponse, user)) - { - response.Success = true; - return response; - } - - var httpClient = _httpClientFactory.CreateClient("HCaptchaValidationService"); - - var requestMessage = new HttpRequestMessage - { - Method = HttpMethod.Post, - RequestUri = new Uri("https://hcaptcha.com/siteverify"), - Content = new FormUrlEncodedContent(new Dictionary - { - { "response", captchaResponse.TrimStart("hcaptcha|".ToCharArray()) }, - { "secret", _globalSettings.Captcha.HCaptchaSecretKey }, - { "sitekey", SiteKey }, - { "remoteip", clientIpAddress } - }) - }; - - HttpResponseMessage responseMessage; - try - { - responseMessage = await httpClient.SendAsync(requestMessage); - } - catch (Exception e) - { - _logger.LogError(11389, e, "Unable to verify with HCaptcha."); - return response; - } - - if (!responseMessage.IsSuccessStatusCode) - { - return response; - } - - using var hcaptchaResponse = await responseMessage.Content.ReadFromJsonAsync(); - response.Success = hcaptchaResponse.Success; - var score = hcaptchaResponse.Score.GetValueOrDefault(); - response.MaybeBot = score >= _globalSettings.Captcha.MaybeBotScoreThreshold; - response.IsBot = score >= _globalSettings.Captcha.IsBotScoreThreshold; - response.Score = score; - return response; - } - - public bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null) - { - if (user == null) - { - return currentContext.IsBot || _globalSettings.Captcha.ForceCaptchaRequired; - } - - var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts; - var failedLoginCount = user?.FailedLoginCount ?? 0; - var requireOnCloud = !_globalSettings.SelfHosted && !user.EmailVerified && - user.CreationDate < DateTime.UtcNow.AddHours(-24); - return currentContext.IsBot || - _globalSettings.Captcha.ForceCaptchaRequired || - requireOnCloud || - failedLoginCeiling > 0 && failedLoginCount >= failedLoginCeiling; - } - - private static bool TokenIsValidApiKey(string bypassToken, User user) => - !string.IsNullOrWhiteSpace(bypassToken) && user != null && user.ApiKey == bypassToken; - - private bool TokenIsValidCaptchaBypassToken(string encryptedToken, User user) - { - return _tokenizer.TryUnprotect(encryptedToken, out var data) && - data.Valid && data.TokenIsValid(user); - } - - private bool ValidateCaptchaBypassToken(string bypassToken, User user) => - TokenIsValidApiKey(bypassToken, user) || TokenIsValidCaptchaBypassToken(bypassToken, user); - - public class HCaptchaResponse : IDisposable - { - [JsonPropertyName("success")] - public bool Success { get; set; } - [JsonPropertyName("score")] - public double? Score { get; set; } - [JsonPropertyName("score_reason")] - public List ScoreReason { get; set; } - - public void Dispose() { } - } -} diff --git a/src/Core/Auth/Services/NoopImplementations/NoopCaptchaValidationService.cs b/src/Core/Auth/Services/NoopImplementations/NoopCaptchaValidationService.cs deleted file mode 100644 index 47e1a38567..0000000000 --- a/src/Core/Auth/Services/NoopImplementations/NoopCaptchaValidationService.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Bit.Core.Auth.Models.Business; -using Bit.Core.Context; -using Bit.Core.Entities; - -namespace Bit.Core.Auth.Services; - -public class NoopCaptchaValidationService : ICaptchaValidationService -{ - public string SiteKeyResponseKeyName => null; - public string SiteKey => null; - public bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null) => false; - public string GenerateCaptchaBypassToken(User user) => ""; - public Task ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress, - User user = null) - { - return Task.FromResult(new CaptchaResponse { Success = true }); - } -} diff --git a/src/Core/Auth/Utilities/CaptchaProtectedAttribute.cs b/src/Core/Auth/Utilities/CaptchaProtectedAttribute.cs deleted file mode 100644 index 052f178165..0000000000 --- a/src/Core/Auth/Utilities/CaptchaProtectedAttribute.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Bit.Core.Auth.Models.Api; -using Bit.Core.Auth.Services; -using Bit.Core.Context; -using Bit.Core.Exceptions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.DependencyInjection; - -namespace Bit.Core.Auth.Utilities; - -public class CaptchaProtectedAttribute : ActionFilterAttribute -{ - public string ModelParameterName { get; set; } = "model"; - - public override void OnActionExecuting(ActionExecutingContext context) - { - var currentContext = context.HttpContext.RequestServices.GetRequiredService(); - var captchaValidationService = context.HttpContext.RequestServices.GetRequiredService(); - - if (captchaValidationService.RequireCaptchaValidation(currentContext, null)) - { - var captchaResponse = (context.ActionArguments[ModelParameterName] as ICaptchaProtectedModel)?.CaptchaResponse; - - if (string.IsNullOrWhiteSpace(captchaResponse)) - { - throw new BadRequestException(captchaValidationService.SiteKeyResponseKeyName, captchaValidationService.SiteKey); - } - - var captchaValidationResponse = captchaValidationService.ValidateCaptchaResponseAsync(captchaResponse, - currentContext.IpAddress, null).GetAwaiter().GetResult(); - if (!captchaValidationResponse.Success || captchaValidationResponse.IsBot) - { - throw new BadRequestException("Captcha is invalid. Please refresh and try again"); - } - } - } -} diff --git a/src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.html.hbs b/src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.html.hbs deleted file mode 100644 index 43531ef242..0000000000 --- a/src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.html.hbs +++ /dev/null @@ -1,31 +0,0 @@ -{{#>FullHtmlLayout}} -
- Verify your email address below to finish signing up for your free trial. + {{VerifyYourEmailHTMLCopy}}
- - - - - - - - - - - - - - - -
- Additional security has been placed on your Bitwarden account. -
- We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha. -
- Account: {{AffectedEmail}}
- Date: {{TheDate}} at {{TheTime}} {{TimeZone}}
- IP Address: {{IpAddress}}
-
- If this was you, you can remove the captcha requirement by successfully logging in. -
- If this was not you, don't worry. The login attempt was not successful and your account has been given additional protection. -
-{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.text.hbs b/src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.text.hbs deleted file mode 100644 index 3393210e4e..0000000000 --- a/src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.text.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{#>BasicTextLayout}} -Additional security has been placed on your Bitwarden account. - -We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha. - -Account: {{AffectedEmail}} -Date: {{TheDate}} at {{TheTime}} {{TimeZone}} -IP Address: {{IpAddress}} - -If this was you, you can remove the captcha requirement by successfully logging in. - -If this was not you, don't worry. The login attempt was not successful and your account has been given additional protection. -{{/BasicTextLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.html.hbs b/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.html.hbs deleted file mode 100644 index d73775f8e8..0000000000 --- a/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.html.hbs +++ /dev/null @@ -1,31 +0,0 @@ -{{#>FullHtmlLayout}} - - - - - - - - - - - - - - - - -
- Additional security has been placed on your Bitwarden account. -
- We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha. -
- Account: {{AffectedEmail}}
- Date: {{TheDate}} at {{TheTime}} {{TimeZone}}
- IP Address: {{IpAddress}}
-
- If this was you, you can remove the captcha requirement by successfully logging in. If you're having trouble with two step login, you can login using a recovery code. -
- If this was not you, you should change your master password immediately. You can view our tips for selecting a secure master password here. -
-{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.text.hbs b/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.text.hbs deleted file mode 100644 index e742d35578..0000000000 --- a/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.text.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{#>BasicTextLayout}} -Additional security has been placed on your Bitwarden account. - -We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha. - -Account: {{AffectedEmail}} -Date: {{TheDate}} at {{TheTime}} {{TimeZone}} -IP Address: {{IpAddress}} - -If this was you, you can remove the captcha requirement by successfully logging in. If you're having trouble with two step login, you can login using a recovery code (https://bitwarden.com/help/two-step-recovery-code/). - -If this was not you, you should change your master password (https://bitwarden.com/help/master-password/#change-master-password) immediately. You can view our tips for selecting a secure master password here (https://bitwarden.com/blog/picking-the-right-password-for-your-password-manager/). -{{/BasicTextLayout}} \ No newline at end of file diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 11d9603a07..7de75a5143 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -88,8 +88,6 @@ public interface IMailService Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail); Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate); Task SendOTPEmailAsync(string email, string token); - Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip); - Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip); Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName); Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName); Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable ownerEmails); diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 3266cc9c2e..315e180721 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -1137,40 +1137,6 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } - public async Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip) - { - var message = CreateDefaultMessage("Failed login attempts detected", email); - var model = new FailedAuthAttemptsModel() - { - TheDate = utcNow.ToLongDateString(), - TheTime = utcNow.ToShortTimeString(), - TimeZone = _utcTimeZoneDisplay, - IpAddress = ip, - AffectedEmail = email - - }; - await AddMessageContentAsync(message, "Auth.FailedLoginAttempts", model); - message.Category = "FailedLoginAttempts"; - await _mailDeliveryService.SendEmailAsync(message); - } - - public async Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip) - { - var message = CreateDefaultMessage("Failed login attempts detected", email); - var model = new FailedAuthAttemptsModel() - { - TheDate = utcNow.ToLongDateString(), - TheTime = utcNow.ToShortTimeString(), - TimeZone = _utcTimeZoneDisplay, - IpAddress = ip, - AffectedEmail = email - - }; - await AddMessageContentAsync(message, "Auth.FailedTwoFactorAttempts", model); - message.Category = "FailedTwoFactorAttempts"; - await _mailDeliveryService.SendEmailAsync(message); - } - public async Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName) { var message = CreateDefaultMessage("Domain not verified", adminEmails); diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index bbad5965f4..83bc3ba7cf 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -268,16 +268,6 @@ public class NoopMailService : IMailService return Task.FromResult(0); } - public Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip) - { - return Task.FromResult(0); - } - - public Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip) - { - return Task.FromResult(0); - } - public Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName) { return Task.FromResult(0); diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 519889db45..d31e18b955 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -45,7 +45,6 @@ public class GlobalSettings : IGlobalSettings public virtual bool EnableCloudCommunication { get; set; } = false; public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days public virtual string EventGridKey { get; set; } - public virtual CaptchaSettings Captcha { get; set; } = new CaptchaSettings(); public virtual IInstallationSettings Installation { get; set; } = new InstallationSettings(); public virtual IBaseServiceUriSettings BaseServiceUri { get; set; } public virtual string DatabaseProvider { get; set; } @@ -629,16 +628,6 @@ public class GlobalSettings : IGlobalSettings public bool EnforceSsoPolicyForAllUsers { get; set; } } - public class CaptchaSettings - { - public bool ForceCaptchaRequired { get; set; } = false; - public string HCaptchaSecretKey { get; set; } - public string HCaptchaSiteKey { get; set; } - public int MaximumFailedLoginAttempts { get; set; } - public double MaybeBotScoreThreshold { get; set; } = double.MaxValue; - public double IsBotScoreThreshold { get; set; } = double.MaxValue; - } - public class StripeSettings { public string ApiKey { get; set; } diff --git a/src/Identity/Controllers/AccountsController.cs b/src/Identity/Controllers/AccountsController.cs index fd42074359..80e9536ea3 100644 --- a/src/Identity/Controllers/AccountsController.cs +++ b/src/Identity/Controllers/AccountsController.cs @@ -5,7 +5,6 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Api.Response.Accounts; using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Auth.Services; using Bit.Core.Auth.UserFeatures.Registration; using Bit.Core.Auth.UserFeatures.WebAuthnLogin; using Bit.Core.Context; @@ -37,7 +36,6 @@ public class AccountsController : Controller private readonly ILogger _logger; private readonly IUserRepository _userRepository; private readonly IRegisterUserCommand _registerUserCommand; - private readonly ICaptchaValidationService _captchaValidationService; private readonly IDataProtectorTokenFactory _assertionOptionsDataProtector; private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand; private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand; @@ -85,7 +83,6 @@ public class AccountsController : Controller ILogger logger, IUserRepository userRepository, IRegisterUserCommand registerUserCommand, - ICaptchaValidationService captchaValidationService, IDataProtectorTokenFactory assertionOptionsDataProtector, IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand, ISendVerificationEmailForRegistrationCommand sendVerificationEmailForRegistrationCommand, @@ -99,7 +96,6 @@ public class AccountsController : Controller _logger = logger; _userRepository = userRepository; _registerUserCommand = registerUserCommand; - _captchaValidationService = captchaValidationService; _assertionOptionsDataProtector = assertionOptionsDataProtector; _getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand; _sendVerificationEmailForRegistrationCommand = sendVerificationEmailForRegistrationCommand; @@ -167,7 +163,7 @@ public class AccountsController : Controller } [HttpPost("register/finish")] - public async Task PostRegisterFinish([FromBody] RegisterFinishRequestModel model) + public async Task PostRegisterFinish([FromBody] RegisterFinishRequestModel model) { var user = model.ToUser(); @@ -208,12 +204,11 @@ public class AccountsController : Controller } } - private RegisterResponseModel ProcessRegistrationResult(IdentityResult result, User user) + private RegisterFinishResponseModel ProcessRegistrationResult(IdentityResult result, User user) { if (result.Succeeded) { - var captchaBypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user); - return new RegisterResponseModel(captchaBypassToken); + return new RegisterFinishResponseModel(); } foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName")) diff --git a/src/Identity/IdentityServer/CustomValidatorRequestContext.cs b/src/Identity/IdentityServer/CustomValidatorRequestContext.cs index bce460c5c4..eb441e7941 100644 --- a/src/Identity/IdentityServer/CustomValidatorRequestContext.cs +++ b/src/Identity/IdentityServer/CustomValidatorRequestContext.cs @@ -1,5 +1,4 @@ -using Bit.Core.Auth.Models.Business; -using Bit.Core.Entities; +using Bit.Core.Entities; using Duende.IdentityServer.Validation; namespace Bit.Identity.IdentityServer; @@ -9,7 +8,7 @@ public class CustomValidatorRequestContext public User User { get; set; } /// /// This is the device that the user is using to authenticate. It can be either known or unknown. - /// We set it here since the ResourceOwnerPasswordValidator needs the device to know if CAPTCHA is required. + /// We set it here since the ResourceOwnerPasswordValidator needs the device to do device validation. /// The option to set it here saves a trip to the database. /// public Device Device { get; set; } @@ -39,5 +38,4 @@ public class CustomValidatorRequestContext /// This will be null if the authentication request is successful. ///
public Dictionary CustomResponse { get; set; } - public CaptchaResponse CaptchaResponse { get; set; } } diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index 8b7034c9d7..9afdcacf14 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -29,7 +29,6 @@ public abstract class BaseRequestValidator where T : class private readonly IDeviceValidator _deviceValidator; private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IMailService _mailService; private readonly ILogger _logger; private readonly GlobalSettings _globalSettings; private readonly IUserRepository _userRepository; @@ -49,7 +48,6 @@ public abstract class BaseRequestValidator where T : class IDeviceValidator deviceValidator, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IMailService mailService, ILogger logger, ICurrentContext currentContext, GlobalSettings globalSettings, @@ -66,7 +64,6 @@ public abstract class BaseRequestValidator where T : class _deviceValidator = deviceValidator; _twoFactorAuthenticationValidator = twoFactorAuthenticationValidator; _organizationUserRepository = organizationUserRepository; - _mailService = mailService; _logger = logger; CurrentContext = currentContext; _globalSettings = globalSettings; @@ -81,23 +78,12 @@ public abstract class BaseRequestValidator where T : class protected async Task ValidateAsync(T context, ValidatedTokenRequest request, CustomValidatorRequestContext validatorContext) { - // 1. We need to check if the user is a bot and if their master password hash is correct. - var isBot = validatorContext.CaptchaResponse?.IsBot ?? false; + // 1. We need to check if the user's master password hash is correct. var valid = await ValidateContextAsync(context, validatorContext); var user = validatorContext.User; - if (!valid || isBot) + if (!valid) { - if (isBot) - { - _logger.LogInformation(Constants.BypassFiltersEventId, - "Login attempt for {UserName} detected as a captcha bot with score {CaptchaScore}.", - request.UserName, validatorContext.CaptchaResponse.Score); - } - - if (!valid) - { - await UpdateFailedAuthDetailsAsync(user, false, !validatorContext.KnownDevice); - } + await UpdateFailedAuthDetailsAsync(user); await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user); return; @@ -167,7 +153,7 @@ public abstract class BaseRequestValidator where T : class } else { - await UpdateFailedAuthDetailsAsync(user, true, !validatorContext.KnownDevice); + await UpdateFailedAuthDetailsAsync(user); await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user); } return; @@ -379,7 +365,7 @@ public abstract class BaseRequestValidator where T : class await _userRepository.ReplaceAsync(user); } - private async Task UpdateFailedAuthDetailsAsync(User user, bool twoFactorInvalid, bool unknownDevice) + private async Task UpdateFailedAuthDetailsAsync(User user) { if (user == null) { @@ -390,32 +376,6 @@ public abstract class BaseRequestValidator where T : class user.FailedLoginCount = ++user.FailedLoginCount; user.LastFailedLoginDate = user.RevisionDate = utcNow; await _userRepository.ReplaceAsync(user); - - if (ValidateFailedAuthEmailConditions(unknownDevice, user)) - { - if (twoFactorInvalid) - { - await _mailService.SendFailedTwoFactorAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress); - } - else - { - await _mailService.SendFailedLoginAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress); - } - } - } - - /// - /// checks to see if a user is trying to log into a new device - /// and has reached the maximum number of failed login attempts. - /// - /// boolean - /// current user - /// - private bool ValidateFailedAuthEmailConditions(bool unknownDevice, User user) - { - var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts; - var failedLoginCount = user?.FailedLoginCount ?? 0; - return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling; } private async Task GetMasterPasswordPolicyAsync(User user) diff --git a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs index 841cd14137..6f2d81bd1b 100644 --- a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs @@ -35,7 +35,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator logger, ICurrentContext currentContext, GlobalSettings globalSettings, @@ -53,7 +52,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator _userManager; private readonly ICurrentContext _currentContext; - private readonly ICaptchaValidationService _captchaValidationService; private readonly IAuthRequestRepository _authRequestRepository; private readonly IDeviceValidator _deviceValidator; public ResourceOwnerPasswordValidator( @@ -31,11 +29,9 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator logger, ICurrentContext currentContext, GlobalSettings globalSettings, - ICaptchaValidationService captchaValidationService, IAuthRequestRepository authRequestRepository, IUserRepository userRepository, IPolicyService policyService, @@ -50,7 +46,6 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator - { - { _captchaValidationService.SiteKeyResponseKeyName, _captchaValidationService.SiteKey }, - }); - return; - } - - validatorContext.CaptchaResponse = await _captchaValidationService.ValidateCaptchaResponseAsync( - captchaResponse, _currentContext.IpAddress, user); - if (!validatorContext.CaptchaResponse.Success) - { - await BuildErrorResultAsync("Captcha is invalid. Please refresh and try again", false, context, null); - return; - } - bypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user); - } - await ValidateAsync(context, context.Request, validatorContext); - if (context.Result.CustomResponse != null && bypassToken != null) - { - context.Result.CustomResponse["CaptchaBypassToken"] = bypassToken; - } } protected async override Task ValidateContextAsync(ResourceOwnerPasswordValidationContext context, diff --git a/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs b/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs index 654edeabe8..76949eb5f7 100644 --- a/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs @@ -35,7 +35,6 @@ public class WebAuthnGrantValidator : BaseRequestValidator logger, ICurrentContext currentContext, GlobalSettings globalSettings, @@ -54,7 +53,6 @@ public class WebAuthnGrantValidator : BaseRequestValidator>>()) ); - services.AddSingleton>(serviceProvider => - new DataProtectorTokenFactory( - HCaptchaTokenable.ClearTextPrefix, - HCaptchaTokenable.DataProtectorPurpose, - serviceProvider.GetDataProtectionProvider(), - serviceProvider.GetRequiredService>>()) - ); - services.AddSingleton>(serviceProvider => new DataProtectorTokenFactory( SsoTokenable.ClearTextPrefix, @@ -401,16 +393,6 @@ public static class ServiceCollectionExtensions { services.AddSingleton(); } - - if (CoreHelpers.SettingHasValue(globalSettings.Captcha?.HCaptchaSecretKey) && - CoreHelpers.SettingHasValue(globalSettings.Captcha?.HCaptchaSiteKey)) - { - services.AddSingleton(); - } - else - { - services.AddSingleton(); - } } public static void AddOosServices(this IServiceCollection services) diff --git a/test/Core.Test/Auth/Models/Business/Tokenables/HCaptchaTokenableTests.cs b/test/Core.Test/Auth/Models/Business/Tokenables/HCaptchaTokenableTests.cs deleted file mode 100644 index 56533bab7a..0000000000 --- a/test/Core.Test/Auth/Models/Business/Tokenables/HCaptchaTokenableTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -using AutoFixture.Xunit2; -using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Entities; -using Bit.Core.Tokens; -using Bit.Test.Common.AutoFixture.Attributes; -using Xunit; - -namespace Bit.Core.Test.Auth.Models.Business.Tokenables; - -public class HCaptchaTokenableTests -{ - [Fact] - public void CanHandleNullUser() - { - var token = new HCaptchaTokenable(null); - - Assert.Equal(default, token.Id); - Assert.Equal(default, token.Email); - } - - [Fact] - public void TokenWithNullUserIsInvalid() - { - var token = new HCaptchaTokenable(null) - { - ExpirationDate = DateTime.UtcNow + TimeSpan.FromDays(1) - }; - - Assert.False(token.Valid); - } - - [Theory, BitAutoData] - public void TokenValidityCheckNullUserIdIsInvalid(User user) - { - var token = new HCaptchaTokenable(user) - { - ExpirationDate = DateTime.UtcNow + TimeSpan.FromDays(1) - }; - - Assert.False(token.TokenIsValid(null)); - } - - [Theory, AutoData] - public void CanUpdateExpirationToNonStandard(User user) - { - var token = new HCaptchaTokenable(user) - { - ExpirationDate = DateTime.MinValue - }; - - Assert.Equal(DateTime.MinValue, token.ExpirationDate, TimeSpan.FromMilliseconds(10)); - } - - [Theory, AutoData] - public void SetsDataFromUser(User user) - { - var token = new HCaptchaTokenable(user); - - Assert.Equal(user.Id, token.Id); - Assert.Equal(user.Email, token.Email); - } - - [Theory, AutoData] - public void SerializationSetsCorrectDateTime(User user) - { - var expectedDateTime = DateTime.UtcNow.AddHours(-5); - var token = new HCaptchaTokenable(user) - { - ExpirationDate = expectedDateTime - }; - - var result = Tokenable.FromToken(token.ToToken()); - - Assert.Equal(expectedDateTime, result.ExpirationDate, TimeSpan.FromMilliseconds(10)); - } - - [Theory, AutoData] - public void IsInvalidIfIdentifierIsWrong(User user) - { - var token = new HCaptchaTokenable(user) - { - Identifier = "not correct" - }; - - Assert.False(token.Valid); - } -} diff --git a/test/Core.Test/Auth/Models/Business/Tokenables/SsoTokenableTests.cs b/test/Core.Test/Auth/Models/Business/Tokenables/SsoTokenableTests.cs index 4d95a1c196..ab393203ab 100644 --- a/test/Core.Test/Auth/Models/Business/Tokenables/SsoTokenableTests.cs +++ b/test/Core.Test/Auth/Models/Business/Tokenables/SsoTokenableTests.cs @@ -67,7 +67,7 @@ public class SsoTokenableTests ExpirationDate = expectedDateTime }; - var result = Tokenable.FromToken(token.ToToken()); + var result = Tokenable.FromToken(token.ToToken()); Assert.Equal(expectedDateTime, result.ExpirationDate, TimeSpan.FromMilliseconds(10)); } diff --git a/test/Identity.Test/Controllers/AccountsControllerTests.cs b/test/Identity.Test/Controllers/AccountsControllerTests.cs index e36f7f37b6..a045490862 100644 --- a/test/Identity.Test/Controllers/AccountsControllerTests.cs +++ b/test/Identity.Test/Controllers/AccountsControllerTests.cs @@ -3,7 +3,6 @@ using System.Text; using Bit.Core; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Auth.Services; using Bit.Core.Auth.UserFeatures.Registration; using Bit.Core.Auth.UserFeatures.WebAuthnLogin; using Bit.Core.Context; @@ -38,7 +37,6 @@ public class AccountsControllerTests : IDisposable private readonly ILogger _logger; private readonly IUserRepository _userRepository; private readonly IRegisterUserCommand _registerUserCommand; - private readonly ICaptchaValidationService _captchaValidationService; private readonly IDataProtectorTokenFactory _assertionOptionsDataProtector; private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand; private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand; @@ -54,7 +52,6 @@ public class AccountsControllerTests : IDisposable _logger = Substitute.For>(); _userRepository = Substitute.For(); _registerUserCommand = Substitute.For(); - _captchaValidationService = Substitute.For(); _assertionOptionsDataProtector = Substitute.For>(); _getWebAuthnLoginCredentialAssertionOptionsCommand = Substitute.For(); _sendVerificationEmailForRegistrationCommand = Substitute.For(); @@ -68,7 +65,6 @@ public class AccountsControllerTests : IDisposable _logger, _userRepository, _registerUserCommand, - _captchaValidationService, _assertionOptionsDataProtector, _getWebAuthnLoginCredentialAssertionOptionsCommand, _sendVerificationEmailForRegistrationCommand, diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index 1d58b62b02..9eb17da88a 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -33,7 +33,6 @@ public class BaseRequestValidatorTests private readonly IDeviceValidator _deviceValidator; private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IMailService _mailService; private readonly ILogger _logger; private readonly ICurrentContext _currentContext; private readonly GlobalSettings _globalSettings; @@ -54,7 +53,6 @@ public class BaseRequestValidatorTests _deviceValidator = Substitute.For(); _twoFactorAuthenticationValidator = Substitute.For(); _organizationUserRepository = Substitute.For(); - _mailService = Substitute.For(); _logger = Substitute.For>(); _currentContext = Substitute.For(); _globalSettings = Substitute.For(); @@ -72,7 +70,6 @@ public class BaseRequestValidatorTests _deviceValidator, _twoFactorAuthenticationValidator, _organizationUserRepository, - _mailService, _logger, _currentContext, _globalSettings, @@ -84,36 +81,6 @@ public class BaseRequestValidatorTests _policyRequirementQuery); } - /* Logic path - * ValidateAsync -> _Logger.LogInformation - * |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync - * |-> SetErrorResult - */ - [Theory, BitAutoData] - public async Task ValidateAsync_IsBot_UserNotNull_ShouldBuildErrorResult_ShouldLogFailedLoginEvent( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, - CustomValidatorRequestContext requestContext, - GrantValidationResult grantResult) - { - // Arrange - var context = CreateContext(tokenRequest, requestContext, grantResult); - - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = true; - _sut.isValid = true; - - // Act - await _sut.ValidateAsync(context); - - var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"]; - - // Assert - await _eventService.Received(1) - .LogUserEventAsync(context.CustomValidatorRequestContext.User.Id, - EventType.User_FailedLogIn); - Assert.True(context.GrantResult.IsError); - Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message); - } - /* Logic path * ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync * |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync @@ -128,8 +95,6 @@ public class BaseRequestValidatorTests { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; - _globalSettings.Captcha.Returns(new GlobalSettings.CaptchaSettings()); _globalSettings.SelfHosted = true; _sut.isValid = false; @@ -142,44 +107,6 @@ public class BaseRequestValidatorTests Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message); } - /* Logic path - * ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync - * |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync - * |-> SetErrorResult - */ - [Theory, BitAutoData] - public async Task ValidateAsync_ContextNotValid_MaxAttemptLogin_ShouldSendEmail( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, - CustomValidatorRequestContext requestContext, - GrantValidationResult grantResult) - { - // Arrange - var context = CreateContext(tokenRequest, requestContext, grantResult); - - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; - // This needs to be n-1 of the max failed login attempts - context.CustomValidatorRequestContext.User.FailedLoginCount = 2; - context.CustomValidatorRequestContext.KnownDevice = false; - - _globalSettings.Captcha.Returns( - new GlobalSettings.CaptchaSettings - { - MaximumFailedLoginAttempts = 3 - }); - _sut.isValid = false; - - // Act - await _sut.ValidateAsync(context); - - // Assert - await _mailService.Received(1) - .SendFailedLoginAttemptsEmailAsync( - Arg.Any(), Arg.Any(), Arg.Any()); - Assert.True(context.GrantResult.IsError); - var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"]; - Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message); - } - [Theory, BitAutoData] public async Task ValidateAsync_DeviceNotValidated_ShouldLogError( [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, @@ -189,7 +116,6 @@ public class BaseRequestValidatorTests // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); // 1 -> to pass - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; // 2 -> will result to false with no extra configuration @@ -226,7 +152,6 @@ public class BaseRequestValidatorTests // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); // 1 -> to pass - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; // 2 -> will result to false with no extra configuration @@ -263,7 +188,6 @@ public class BaseRequestValidatorTests { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; context.ValidatedTokenRequest.GrantType = grantType; @@ -294,7 +218,6 @@ public class BaseRequestValidatorTests // Arrange _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); var context = CreateContext(tokenRequest, requestContext, grantResult); - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; context.ValidatedTokenRequest.GrantType = grantType; @@ -326,7 +249,6 @@ public class BaseRequestValidatorTests // Arrange _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); var context = CreateContext(tokenRequest, requestContext, grantResult); - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; context.ValidatedTokenRequest.GrantType = grantType; @@ -363,7 +285,6 @@ public class BaseRequestValidatorTests { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; context.ValidatedTokenRequest.GrantType = grantType; @@ -401,7 +322,6 @@ public class BaseRequestValidatorTests { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; context.ValidatedTokenRequest.GrantType = grantType; @@ -439,7 +359,6 @@ public class BaseRequestValidatorTests var user = context.CustomValidatorRequestContext.User; user.Key = null; - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; context.ValidatedTokenRequest.ClientId = "Not Web"; _sut.isValid = true; _twoFactorAuthenticationValidator diff --git a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs index c204e380b8..4c14de2d73 100644 --- a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs +++ b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs @@ -54,7 +54,6 @@ IBaseRequestValidatorTestWrapper IDeviceValidator deviceValidator, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IMailService mailService, ILogger logger, ICurrentContext currentContext, GlobalSettings globalSettings, @@ -71,7 +70,6 @@ IBaseRequestValidatorTestWrapper deviceValidator, twoFactorAuthenticationValidator, organizationUserRepository, - mailService, logger, currentContext, globalSettings, diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs index c1089608da..76fa0f03d1 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs @@ -1,5 +1,4 @@ using AspNetCoreRateLimit; -using Bit.Core.Auth.Services; using Bit.Core.Billing.Services; using Bit.Core.Platform.Push; using Bit.Core.Platform.Push.Internal; @@ -207,8 +206,6 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory Replace(services); - Replace(services); - // TODO: Install and use azurite in CI pipeline Replace(services); diff --git a/util/Setup/Configuration.cs b/util/Setup/Configuration.cs index 264eef05b2..3372652d03 100644 --- a/util/Setup/Configuration.cs +++ b/util/Setup/Configuration.cs @@ -31,9 +31,6 @@ public class Configuration "Learn more: https://docs.docker.com/compose/compose-file/#ports")] public string HttpsPort { get; set; } = "443"; - [Description("Configure Nginx for Captcha.")] - public bool Captcha { get; set; } = false; - [Description("Configure Nginx for SSL.")] public bool Ssl { get; set; } = true; diff --git a/util/Setup/NginxConfigBuilder.cs b/util/Setup/NginxConfigBuilder.cs index 865b8bdd69..1315ffaba7 100644 --- a/util/Setup/NginxConfigBuilder.cs +++ b/util/Setup/NginxConfigBuilder.cs @@ -73,7 +73,6 @@ public class NginxConfigBuilder public TemplateModel(Context context) { - Captcha = context.Config.Captcha; Ssl = context.Config.Ssl; EnableKeyConnector = context.Config.EnableKeyConnector; EnableScim = context.Config.EnableScim; @@ -127,7 +126,6 @@ public class NginxConfigBuilder } } - public bool Captcha { get; set; } public bool Ssl { get; set; } public bool EnableKeyConnector { get; set; } public bool EnableScim { get; set; } diff --git a/util/Setup/Templates/NginxConfig.hbs b/util/Setup/Templates/NginxConfig.hbs index 115c79c72a..f37987ca70 100644 --- a/util/Setup/Templates/NginxConfig.hbs +++ b/util/Setup/Templates/NginxConfig.hbs @@ -100,16 +100,6 @@ server { proxy_pass http://web:5000/sso-connector.html; } -{{#if Captcha}} - location = /captcha-connector.html { - proxy_pass http://web:5000/captcha-connector.html; - } - - location = /captcha-mobile-connector.html { - proxy_pass http://web:5000/captcha-mobile-connector.html; - } -{{/if}} - location /attachments/ { proxy_pass http://attachments:5000/; } From 3f95513d1142d02cdc6a0f36e7a24dd7f25c9f69 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Fri, 9 May 2025 11:39:57 -0400 Subject: [PATCH 15/20] [PM-19029][PM-19203] Addressing `UserService` tech debt around `ITwoFactorIsEnabledQuery` (#5754) * fix : split out the interface from the TwoFactorAuthenticationValidator into separate file. * fix: replacing IUserService.TwoFactorEnabled with ITwoFactorEnabledQuery * fix: combined logic for both bulk and single user look ups for TwoFactorIsEnabledQuery. * fix: return two factor provider enabled on CanGenerate() method. * tech debt: modfifying MFA providers to call the database less to validate if two factor is enabled. * tech debt: removed unused service from AuthenticatorTokenProvider * doc: added documentation to ITwoFactorProviderUsers * doc: updated comments for TwoFactorIsEnabled impl * test: fixing tests for ITwoFactorIsEnabledQuery * test: updating tests to have correct DI and removing test for automatic email of TOTP. * test: adding better test coverage --- .../Public/Controllers/MembersController.cs | 4 +- .../Auth/Controllers/AccountsController.cs | 10 +- .../Billing/Controllers/AccountsController.cs | 6 +- src/Api/Vault/Controllers/SyncController.cs | 8 +- .../OrganizationUsers/AcceptOrgUserCommand.cs | 6 +- src/Core/Auth/Enums/TwoFactorProviderType.cs | 3 +- .../AuthenticatorTokenProvider.cs | 15 +-- .../DuoUniversalTokenProvider.cs | 11 +- .../EmailTwoFactorTokenProvider.cs | 17 +-- .../TokenProviders/WebAuthnTokenProvider.cs | 19 ++- .../TokenProviders/YubicoOtpTokenProvider.cs | 8 +- src/Core/Auth/Identity/UserStore.cs | 6 +- .../Auth/Models/ITwoFactorProvidersUser.cs | 8 ++ .../Interfaces/ITwoFactorIsEnabledQuery.cs | 4 +- .../TwoFactorAuth/TwoFactorIsEnabledQuery.cs | 96 +++++++------- src/Core/Entities/User.cs | 45 ++++--- src/Core/Repositories/IUserRepository.cs | 10 ++ src/Core/Services/IUserService.cs | 8 +- .../Services/Implementations/UserService.cs | 48 +------ .../ITwoFactorAuthenticationValidator.cs | 38 ++++++ .../TwoFactorAuthenticationValidator.cs | 44 +------ .../Repositories/UserRepository.cs | 10 +- .../Repositories/UserRepository.cs | 16 ++- .../Controllers/AccountsControllerTests.cs | 4 + .../Vault/Controllers/SyncControllerTests.cs | 19 ++- .../AcceptOrgUserCommandTests.cs | 6 +- .../Auth/Identity/BaseTokenProviderTests.cs | 5 - ...DuoUniversalTwoFactorTokenProviderTests.cs | 3 + .../TwoFactorIsEnabledQueryTests.cs | 120 +++++++++++++++--- test/Core.Test/Services/UserServiceTests.cs | 6 + .../TwoFactorAuthenticationValidatorTests.cs | 28 +--- 31 files changed, 372 insertions(+), 259 deletions(-) create mode 100644 src/Identity/IdentityServer/RequestValidators/ITwoFactorAuthenticationValidator.cs diff --git a/src/Api/AdminConsole/Public/Controllers/MembersController.cs b/src/Api/AdminConsole/Public/Controllers/MembersController.cs index 92e5071801..6552684ca3 100644 --- a/src/Api/AdminConsole/Public/Controllers/MembersController.cs +++ b/src/Api/AdminConsole/Public/Controllers/MembersController.cs @@ -76,7 +76,7 @@ public class MembersController : Controller { return new NotFoundResult(); } - var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser), + var response = new MemberResponseModel(orgUser, await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUser), collections); return new JsonResult(response); } @@ -185,7 +185,7 @@ public class MembersController : Controller { var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id); response = new MemberResponseModel(existingUserDetails, - await _userService.TwoFactorIsEnabledAsync(existingUserDetails), associations); + await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(existingUserDetails), associations); } else { diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 2134a7fc4e..fdd5fbb290 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -16,6 +16,7 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Entities; using Bit.Core.Enums; @@ -45,6 +46,7 @@ public class AccountsController : Controller private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand; private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand; private readonly IRotateUserKeyCommand _rotateUserKeyCommand; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IFeatureService _featureService; private readonly IRotationValidator, IEnumerable> _cipherValidator; @@ -68,6 +70,7 @@ public class AccountsController : Controller ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand, ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand, IRotateUserKeyCommand rotateUserKeyCommand, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IFeatureService featureService, IRotationValidator, IEnumerable> cipherValidator, IRotationValidator, IEnumerable> folderValidator, @@ -87,6 +90,7 @@ public class AccountsController : Controller _setInitialMasterPasswordCommand = setInitialMasterPasswordCommand; _tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand; _rotateUserKeyCommand = rotateUserKeyCommand; + _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _featureService = featureService; _cipherValidator = cipherValidator; _folderValidator = folderValidator; @@ -389,7 +393,7 @@ public class AccountsController : Controller await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed); - var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); @@ -423,7 +427,7 @@ public class AccountsController : Controller await _userService.SaveUserAsync(model.ToUser(user)); - var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); @@ -442,7 +446,7 @@ public class AccountsController : Controller } await _userService.SaveUserAsync(model.ToUser(user), true); - var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); diff --git a/src/Api/Billing/Controllers/AccountsController.cs b/src/Api/Billing/Controllers/AccountsController.cs index bc263691a8..49ff679bb8 100644 --- a/src/Api/Billing/Controllers/AccountsController.cs +++ b/src/Api/Billing/Controllers/AccountsController.cs @@ -3,6 +3,7 @@ using Bit.Api.Models.Request; using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Response; using Bit.Api.Utilities; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Models; using Bit.Core.Billing.Services; using Bit.Core.Context; @@ -22,7 +23,8 @@ namespace Bit.Api.Billing.Controllers; [Route("accounts")] [Authorize("Application")] public class AccountsController( - IUserService userService) : Controller + IUserService userService, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) : Controller { [HttpPost("premium")] public async Task PostPremiumAsync( @@ -56,7 +58,7 @@ public class AccountsController( model.PaymentMethodType!.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license, new TaxInfo { BillingAddressCountry = model.Country, BillingAddressPostalCode = model.PostalCode }); - var userTwoFactorEnabled = await userService.TwoFactorIsEnabledAsync(user); + var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index 4b66c7f2bd..568c05d651 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -3,6 +3,7 @@ using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -37,6 +38,7 @@ public class SyncController : Controller private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion); private readonly IFeatureService _featureService; private readonly IApplicationCacheService _applicationCacheService; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; public SyncController( IUserService userService, @@ -51,7 +53,8 @@ public class SyncController : Controller GlobalSettings globalSettings, ICurrentContext currentContext, IFeatureService featureService, - IApplicationCacheService applicationCacheService) + IApplicationCacheService applicationCacheService, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) { _userService = userService; _folderRepository = folderRepository; @@ -66,6 +69,7 @@ public class SyncController : Controller _currentContext = currentContext; _featureService = featureService; _applicationCacheService = applicationCacheService; + _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; } [HttpGet("")] @@ -102,7 +106,7 @@ public class SyncController : Controller collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key); } - var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); var organizationClaimingActiveUser = await _userService.GetOrganizationsClaimingUserAsync(user.Id); var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs index 756bd2ae46..f3426efddc 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; @@ -24,6 +25,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand private readonly IPolicyService _policyService; private readonly IMailService _mailService; private readonly IUserRepository _userRepository; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; public AcceptOrgUserCommand( @@ -34,6 +36,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand IPolicyService policyService, IMailService mailService, IUserRepository userRepository, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IDataProtectorTokenFactory orgUserInviteTokenDataFactory) { @@ -45,6 +48,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand _policyService = policyService; _mailService = mailService; _userRepository = userRepository; + _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; } @@ -192,7 +196,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand } // Enforce Two Factor Authentication Policy of organization user is trying to join - if (!await userService.TwoFactorIsEnabledAsync(user)) + if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user)) { var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); diff --git a/src/Core/Auth/Enums/TwoFactorProviderType.cs b/src/Core/Auth/Enums/TwoFactorProviderType.cs index 07a52dc429..c3613785bc 100644 --- a/src/Core/Auth/Enums/TwoFactorProviderType.cs +++ b/src/Core/Auth/Enums/TwoFactorProviderType.cs @@ -6,7 +6,8 @@ public enum TwoFactorProviderType : byte Email = 1, Duo = 2, YubiKey = 3, - U2f = 4, // Deprecated + [Obsolete("Deprecated in favor of WebAuthn.")] + U2f = 4, Remember = 5, OrganizationDuo = 6, WebAuthn = 7, diff --git a/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs index 9468e4d571..5a3d9522f3 100644 --- a/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs @@ -1,6 +1,5 @@ using Bit.Core.Auth.Enums; using Bit.Core.Entities; -using Bit.Core.Services; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; @@ -12,16 +11,13 @@ public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider { private const string CacheKeyFormat = "Authenticator_TOTP_{0}_{1}"; - private readonly IServiceProvider _serviceProvider; private readonly IDistributedCache _distributedCache; private readonly DistributedCacheEntryOptions _distributedCacheEntryOptions; public AuthenticatorTokenProvider( - IServiceProvider serviceProvider, [FromKeyedServices("persistent")] IDistributedCache distributedCache) { - _serviceProvider = serviceProvider; _distributedCache = distributedCache; _distributedCacheEntryOptions = new DistributedCacheEntryOptions { @@ -29,15 +25,14 @@ public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider }; } - public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) + public Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator); - if (string.IsNullOrWhiteSpace((string)provider?.MetaData["Key"])) + var authenticatorProvider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator); + if (string.IsNullOrWhiteSpace((string)authenticatorProvider?.MetaData["Key"])) { - return false; + return Task.FromResult(false); } - return await _serviceProvider.GetRequiredService() - .TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Authenticator, user); + return Task.FromResult(authenticatorProvider.Enabled); } public Task GenerateAsync(string purpose, UserManager manager, User user) diff --git a/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs index cbb994fa09..3f2a44915c 100644 --- a/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs @@ -17,7 +17,7 @@ public class DuoUniversalTokenProvider( { /// /// We need the IServiceProvider to resolve the . There is a complex dependency dance - /// occurring between , which extends the , and the usage + /// occurring between , which extends the , and the usage /// of the within this class. Trying to resolve the using /// the DI pipeline will not allow the server to start and it will hang and give no helpful indication as to the /// problem. @@ -29,12 +29,13 @@ public class DuoUniversalTokenProvider( public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { var userService = _serviceProvider.GetRequiredService(); - var provider = await GetDuoTwoFactorProvider(user, userService); - if (provider == null) + var duoUniversalTokenProvider = await GetDuoTwoFactorProvider(user, userService); + if (duoUniversalTokenProvider == null) { return false; } - return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Duo, user); + + return duoUniversalTokenProvider.Enabled; } public async Task GenerateAsync(string purpose, UserManager manager, User user) @@ -58,7 +59,7 @@ public class DuoUniversalTokenProvider( } /// - /// Get the Duo Two Factor Provider for the user if they have access to Duo + /// Get the Duo Two Factor Provider for the user if they have premium access to Duo /// /// Active User /// null or Duo TwoFactorProvider diff --git a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs index b0ad9bd480..718e44ae5f 100644 --- a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs @@ -1,7 +1,6 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Entities; -using Bit.Core.Services; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; @@ -10,31 +9,25 @@ namespace Bit.Core.Auth.Identity.TokenProviders; public class EmailTwoFactorTokenProvider : EmailTokenProvider { - private readonly IServiceProvider _serviceProvider; - public EmailTwoFactorTokenProvider( - IServiceProvider serviceProvider, [FromKeyedServices("persistent")] IDistributedCache distributedCache) : base(distributedCache) { - _serviceProvider = serviceProvider; - TokenAlpha = false; TokenNumeric = true; TokenLength = 6; } - public override async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) + public override Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); - if (!HasProperMetaData(provider)) + var emailTokenProvider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); + if (!HasProperMetaData(emailTokenProvider)) { - return false; + return Task.FromResult(false); } - return await _serviceProvider.GetRequiredService(). - TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Email, user); + return Task.FromResult(emailTokenProvider.Enabled); } public override Task GenerateAsync(string purpose, UserManager manager, User user) diff --git a/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs index 202ba3a38c..0bf75d0fc3 100644 --- a/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs @@ -25,17 +25,16 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider _globalSettings = globalSettings; } - public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) + public Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { - var userService = _serviceProvider.GetRequiredService(); - var webAuthnProvider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); + // null check happens in this method if (!HasProperMetaData(webAuthnProvider)) { - return false; + return Task.FromResult(false); } - return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.WebAuthn, user); + return Task.FromResult(webAuthnProvider.Enabled); } public async Task GenerateAsync(string purpose, UserManager manager, User user) @@ -81,7 +80,7 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); var keys = LoadKeys(provider); - if (!provider.MetaData.ContainsKey("login")) + if (!provider.MetaData.TryGetValue("login", out var value)) { return false; } @@ -89,7 +88,7 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider var clientResponse = JsonSerializer.Deserialize(token, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - var jsonOptions = provider.MetaData["login"].ToString(); + var jsonOptions = value.ToString(); var options = AssertionOptions.FromJson(jsonOptions); var webAuthCred = keys.Find(k => k.Item2.Descriptor.Id.SequenceEqual(clientResponse.Id)); @@ -126,6 +125,12 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider } + /// + /// Checks if the provider has proper metadata. + /// This is used to determine if the provider has been properly configured. + /// + /// + /// true if metadata is present; false if empty or null private bool HasProperMetaData(TwoFactorProvider provider) { return provider?.MetaData?.Any() ?? false; diff --git a/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs index 9794a51ae9..b33d2fc0c9 100644 --- a/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs @@ -23,19 +23,21 @@ public class YubicoOtpTokenProvider : IUserTwoFactorTokenProvider public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { + // Ensure the user has access to premium var userService = _serviceProvider.GetRequiredService(); if (!await userService.CanAccessPremium(user)) { return false; } - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.YubiKey); - if (!provider?.MetaData.Values.Any(v => !string.IsNullOrWhiteSpace((string)v)) ?? true) + // Check if the user has a YubiKey provider configured + var yubicoProvider = user.GetTwoFactorProvider(TwoFactorProviderType.YubiKey); + if (!yubicoProvider?.MetaData.Values.Any(v => !string.IsNullOrWhiteSpace((string)v)) ?? true) { return false; } - return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.YubiKey, user); + return yubicoProvider.Enabled; } public Task GenerateAsync(string purpose, UserManager manager, User user) diff --git a/src/Core/Auth/Identity/UserStore.cs b/src/Core/Auth/Identity/UserStore.cs index 3716d75b6a..41323f05b7 100644 --- a/src/Core/Auth/Identity/UserStore.cs +++ b/src/Core/Auth/Identity/UserStore.cs @@ -1,7 +1,7 @@ -using Bit.Core.Context; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Repositories; -using Bit.Core.Services; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; @@ -167,7 +167,7 @@ public class UserStore : public async Task GetTwoFactorEnabledAsync(User user, CancellationToken cancellationToken) { - return await _serviceProvider.GetRequiredService().TwoFactorIsEnabledAsync(user); + return await _serviceProvider.GetRequiredService().TwoFactorIsEnabledAsync(user); } public Task SetSecurityStampAsync(User user, string stamp, CancellationToken cancellationToken) diff --git a/src/Core/Auth/Models/ITwoFactorProvidersUser.cs b/src/Core/Auth/Models/ITwoFactorProvidersUser.cs index 5d9ae4b362..f953e4570e 100644 --- a/src/Core/Auth/Models/ITwoFactorProvidersUser.cs +++ b/src/Core/Auth/Models/ITwoFactorProvidersUser.cs @@ -1,10 +1,18 @@ using Bit.Core.Auth.Enums; +using Bit.Core.Services; namespace Bit.Core.Auth.Models; public interface ITwoFactorProvidersUser { string TwoFactorProviders { get; } + /// + /// Get the two factor providers for the user. Currently it can be assumed providers are enabled + /// if they exists in the dictionary. When two factor providers are disabled they are removed + /// from the dictionary. + /// + /// + /// Dictionary of providers with the type enum as the key Dictionary GetTwoFactorProviders(); Guid? GetUserId(); bool GetPremium(); diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/Interfaces/ITwoFactorIsEnabledQuery.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/Interfaces/ITwoFactorIsEnabledQuery.cs index 203ef3accb..697c10690c 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/Interfaces/ITwoFactorIsEnabledQuery.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/Interfaces/ITwoFactorIsEnabledQuery.cs @@ -2,6 +2,7 @@ namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; + public interface ITwoFactorIsEnabledQuery { /// @@ -16,7 +17,8 @@ public interface ITwoFactorIsEnabledQuery /// The type of user in the list. Must implement . Task> TwoFactorIsEnabledAsync(IEnumerable users) where T : ITwoFactorProvidersUser; /// - /// Returns whether two factor is enabled for the user. + /// Returns whether two factor is enabled for the user. A user is able to have a TwoFactorProvider that is enabled but requires Premium. + /// If the user does not have premium then the TwoFactorProvider is considered _not_ enabled. /// /// The user to check. Task TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user); diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs index bda2094f24..8d4bd49e42 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs @@ -1,17 +1,13 @@ -using Bit.Core.Auth.Models; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Repositories; namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth; -public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery +public class TwoFactorIsEnabledQuery(IUserRepository userRepository) : ITwoFactorIsEnabledQuery { - private readonly IUserRepository _userRepository; - - public TwoFactorIsEnabledQuery(IUserRepository userRepository) - { - _userRepository = userRepository; - } + private readonly IUserRepository _userRepository = userRepository; public async Task> TwoFactorIsEnabledAsync(IEnumerable userIds) { @@ -21,26 +17,15 @@ public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery return result; } - var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync(userIds.ToList()); - + var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync([.. userIds]); foreach (var userDetail in userDetails) { - var hasTwoFactor = false; - var providers = userDetail.GetTwoFactorProviders(); - if (providers != null) - { - // Get all enabled providers - var enabledProviderKeys = from provider in providers - where provider.Value?.Enabled ?? false - select provider.Key; - - // Find the first provider that is enabled and passes the premium check - hasTwoFactor = enabledProviderKeys - .Select(type => userDetail.HasPremiumAccess || !TwoFactorProvider.RequiresPremium(type)) - .FirstOrDefault(); - } - - result.Add((userDetail.Id, hasTwoFactor)); + result.Add( + (userDetail.Id, + await TwoFactorEnabledAsync(userDetail.GetTwoFactorProviders(), + () => Task.FromResult(userDetail.HasPremiumAccess)) + ) + ); } return result; @@ -83,41 +68,56 @@ public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery return false; } - var providers = user.GetTwoFactorProviders(); - if (providers == null || !providers.Any()) + return await TwoFactorEnabledAsync( + user.GetTwoFactorProviders(), + async () => + { + var calcUser = await _userRepository.GetCalculatedPremiumAsync(userId.Value); + return calcUser?.HasPremiumAccess ?? false; + }); + } + + /// + /// Checks to see what kind of two-factor is enabled. + /// We use a delegate to check if the user has premium access, since there are multiple ways to + /// determine if a user has premium access. + /// + /// dictionary of two factor providers + /// function to check if the user has premium access + /// true if the user has two factor enabled; false otherwise; + private async static Task TwoFactorEnabledAsync( + Dictionary providers, + Func> hasPremiumAccessDelegate) + { + // If there are no providers, then two factor is not enabled + if (providers == null || providers.Count == 0) { return false; } // Get all enabled providers - var enabledProviderKeys = providers - .Where(provider => provider.Value?.Enabled ?? false) - .Select(provider => provider.Key); + // TODO: PM-21210: In practice we don't save disabled providers to the database, worth looking into. + var enabledProviderKeys = from provider in providers + where provider.Value?.Enabled ?? false + select provider.Key; + // If no providers are enabled then two factor is not enabled if (!enabledProviderKeys.Any()) { return false; } - // Determine if any enabled provider passes the premium check - var hasTwoFactor = enabledProviderKeys - .Select(type => user.GetPremium() || !TwoFactorProvider.RequiresPremium(type)) - .FirstOrDefault(); - - // If no enabled provider passes the check, check the repository for organization premium access - if (!hasTwoFactor) + // If there are only premium two factor options then standard two factor is not enabled + var onlyHasPremiumTwoFactor = enabledProviderKeys.All(TwoFactorProvider.RequiresPremium); + if (onlyHasPremiumTwoFactor) { - var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync(new List { userId.Value }); - var userDetail = userDetails.FirstOrDefault(); - - if (userDetail != null) - { - hasTwoFactor = enabledProviderKeys - .Select(type => userDetail.HasPremiumAccess || !TwoFactorProvider.RequiresPremium(type)) - .FirstOrDefault(); - } + // There are no Standard two factor options, check if the user has premium access + // If the user has premium access, then two factor is enabled + var premiumAccess = await hasPremiumAccessDelegate(); + return premiumAccess; } - return hasTwoFactor; + // The user has at least one non-premium two factor option + return true; } } diff --git a/src/Core/Entities/User.cs b/src/Core/Entities/User.cs index 9878c96c1c..b3a6a9592e 100644 --- a/src/Core/Entities/User.cs +++ b/src/Core/Entities/User.cs @@ -128,6 +128,10 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac public bool IsExpired() => PremiumExpirationDate.HasValue && PremiumExpirationDate.Value <= DateTime.UtcNow; + /// + /// Deserializes the User.TwoFactorProviders property from JSON to the appropriate C# dictionary. + /// + /// Dictionary of TwoFactor providers public Dictionary? GetTwoFactorProviders() { if (string.IsNullOrWhiteSpace(TwoFactorProviders)) @@ -137,19 +141,17 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac try { - if (_twoFactorProviders == null) - { - _twoFactorProviders = - JsonHelpers.LegacyDeserialize>( - TwoFactorProviders); - } + _twoFactorProviders ??= + JsonHelpers.LegacyDeserialize>( + TwoFactorProviders); - // U2F is no longer supported, and all users keys should have been migrated to WebAuthn. - // To prevent issues with accounts being prompted for unsupported U2F we remove them - if (_twoFactorProviders.ContainsKey(TwoFactorProviderType.U2f)) - { - _twoFactorProviders.Remove(TwoFactorProviderType.U2f); - } + /* + U2F is no longer supported, and all users keys should have been migrated to WebAuthn. + To prevent issues with accounts being prompted for unsupported U2F we remove them. + This will probably exist in perpetuity since there is no way to know for sure if any + given user does or doesn't have this enabled. It is a non-zero chance. + */ + _twoFactorProviders?.Remove(TwoFactorProviderType.U2f); return _twoFactorProviders; } @@ -169,6 +171,10 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac return Premium; } + /// + /// Serializes the C# object to the User.TwoFactorProviders property in JSON format. + /// + /// Dictionary of Two Factor providers public void SetTwoFactorProviders(Dictionary providers) { // When replacing with system.text remember to remove the extra serialization in WebAuthnTokenProvider. @@ -176,20 +182,21 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac _twoFactorProviders = providers; } - public void ClearTwoFactorProviders() - { - SetTwoFactorProviders(new Dictionary()); - } - + /// + /// Checks if the user has a specific TwoFactorProvider configured. If a user has a premium TwoFactor + /// configured it will still be found, even if the user's premium subscription has ended. + /// + /// TwoFactor provider being searched for + /// TwoFactorProvider if found; null otherwise. public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider) { var providers = GetTwoFactorProviders(); - if (providers == null || !providers.ContainsKey(provider)) + if (providers == null || !providers.TryGetValue(provider, out var value)) { return null; } - return providers[provider]; + return value; } public long StorageBytesRemaining() diff --git a/src/Core/Repositories/IUserRepository.cs b/src/Core/Repositories/IUserRepository.cs index 0e59b9998f..22effb4329 100644 --- a/src/Core/Repositories/IUserRepository.cs +++ b/src/Core/Repositories/IUserRepository.cs @@ -25,6 +25,16 @@ public interface IUserRepository : IRepository /// Task> GetManyWithCalculatedPremiumAsync(IEnumerable ids); /// + /// Retrieves the data for the requested user ID and includes additional property indicating + /// whether the user has premium access directly or through an organization. + /// + /// Calls the same stored procedure as GetManyWithCalculatedPremiumAsync but handles the query + /// for a single user. + /// + /// The user ID to retrieve data for. + /// User data with calculated premium access; null if nothing is found + Task GetCalculatedPremiumAsync(Guid userId); + /// /// Sets a new user key and updates all encrypted data. /// Warning: Any user key encrypted data not included will be lost. /// diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 9b12713218..228b8543d7 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -71,11 +71,13 @@ public interface IUserService Task GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null, int? version = null); Task CheckPasswordAsync(User user, string password); + /// + /// Checks if the user has access to premium features, either through a personal subscription or through an organization. + /// + /// user being acted on + /// true if they can access premium; false otherwise. Task CanAccessPremium(ITwoFactorProvidersUser user); Task HasPremiumFromOrganization(ITwoFactorProvidersUser user); - [Obsolete("Use ITwoFactorIsEnabledQuery instead.")] - Task TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user); - Task TwoFactorProviderIsEnabledAsync(TwoFactorProviderType provider, ITwoFactorProvidersUser user); Task GenerateSignInTokenAsync(User user, string purpose); Task UpdatePasswordHash(User user, string newPassword, diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 95ee4544fa..23617a0fcd 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -11,6 +11,7 @@ using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; @@ -77,6 +78,7 @@ public class UserService : UserManager, IUserService, IDisposable private readonly IPremiumUserBillingService _premiumUserBillingService; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IDistributedCache _distributedCache; public UserService( @@ -115,6 +117,7 @@ public class UserService : UserManager, IUserService, IDisposable IPremiumUserBillingService premiumUserBillingService, IRemoveOrganizationUserCommand removeOrganizationUserCommand, IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IDistributedCache distributedCache) : base( store, @@ -158,6 +161,7 @@ public class UserService : UserManager, IUserService, IDisposable _premiumUserBillingService = premiumUserBillingService; _removeOrganizationUserCommand = removeOrganizationUserCommand; _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand; + _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _distributedCache = distributedCache; } @@ -918,7 +922,7 @@ public class UserService : UserManager, IUserService, IDisposable await SaveUserAsync(user); await _eventService.LogUserEventAsync(user.Id, EventType.User_Disabled2fa); - if (!await TwoFactorIsEnabledAsync(user)) + if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user)) { await CheckPoliciesOnTwoFactorRemovalAsync(user); } @@ -1280,48 +1284,6 @@ public class UserService : UserManager, IUserService, IDisposable orgAbility.UsersGetPremium && orgAbility.Enabled); } - - public async Task TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user) - { - var providers = user.GetTwoFactorProviders(); - if (providers == null) - { - return false; - } - - foreach (var p in providers) - { - if (p.Value?.Enabled ?? false) - { - if (!TwoFactorProvider.RequiresPremium(p.Key)) - { - return true; - } - if (await CanAccessPremium(user)) - { - return true; - } - } - } - return false; - } - - public async Task TwoFactorProviderIsEnabledAsync(TwoFactorProviderType provider, ITwoFactorProvidersUser user) - { - var providers = user.GetTwoFactorProviders(); - if (providers == null || !providers.ContainsKey(provider) || !providers[provider].Enabled) - { - return false; - } - - if (!TwoFactorProvider.RequiresPremium(provider)) - { - return true; - } - - return await CanAccessPremium(user); - } - public async Task GenerateSignInTokenAsync(User user, string purpose) { var token = await GenerateUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider, diff --git a/src/Identity/IdentityServer/RequestValidators/ITwoFactorAuthenticationValidator.cs b/src/Identity/IdentityServer/RequestValidators/ITwoFactorAuthenticationValidator.cs new file mode 100644 index 0000000000..cc45fcb3eb --- /dev/null +++ b/src/Identity/IdentityServer/RequestValidators/ITwoFactorAuthenticationValidator.cs @@ -0,0 +1,38 @@ + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Entities; +using Duende.IdentityServer.Validation; + +namespace Bit.Identity.IdentityServer.RequestValidators; + +public interface ITwoFactorAuthenticationValidator +{ + /// + /// Check if the user is required to use two-factor authentication to login. This is based on the user's + /// enabled two-factor providers, the user's organizations enabled two-factor providers, and the grant type. + /// Client credentials and webauthn grant types do not require two-factor authentication. + /// + /// the active user for the request + /// the request that contains the grant types + /// boolean + Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request); + /// + /// Builds the two-factor authentication result for the user based on the available two-factor providers + /// from either their user account or Organization. + /// + /// user trying to login + /// organization associated with the user; Can be null + /// Dictionary with the TwoFactorProviderType as the Key and the Provider Metadata as the Value + Task> BuildTwoFactorResultAsync(User user, Organization organization); + /// + /// Uses the built in userManager methods to verify the two-factor token for the user. If the organization uses + /// organization duo, it will use the organization duo token provider to verify the token. + /// + /// the active User + /// organization of user; can be null + /// Two Factor Provider to use to verify the token + /// secret passed from the user and consumed by the two-factor provider's verify method + /// boolean + Task VerifyTwoFactorAsync(User user, Organization organization, TwoFactorProviderType twoFactorProviderType, string token); +} diff --git a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs index e733d4f410..80b3b6e1f4 100644 --- a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs @@ -4,6 +4,7 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Models.Data.Organizations; @@ -16,56 +17,25 @@ using Microsoft.AspNetCore.Identity; namespace Bit.Identity.IdentityServer.RequestValidators; -public interface ITwoFactorAuthenticationValidator -{ - /// - /// Check if the user is required to use two-factor authentication to login. This is based on the user's - /// enabled two-factor providers, the user's organizations enabled two-factor providers, and the grant type. - /// Client credentials and webauthn grant types do not require two-factor authentication. - /// - /// the active user for the request - /// the request that contains the grant types - /// boolean - Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request); - /// - /// Builds the two-factor authentication result for the user based on the available two-factor providers - /// from either their user account or Organization. - /// - /// user trying to login - /// organization associated with the user; Can be null - /// Dictionary with the TwoFactorProviderType as the Key and the Provider Metadata as the Value - Task> BuildTwoFactorResultAsync(User user, Organization organization); - /// - /// Uses the built in userManager methods to verify the two-factor token for the user. If the organization uses - /// organization duo, it will use the organization duo token provider to verify the token. - /// - /// the active User - /// organization of user; can be null - /// Two Factor Provider to use to verify the token - /// secret passed from the user and consumed by the two-factor provider's verify method - /// boolean - Task VerifyTwoFactorAsync(User user, Organization organization, TwoFactorProviderType twoFactorProviderType, string token); -} - public class TwoFactorAuthenticationValidator( IUserService userService, UserManager userManager, IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider, - IFeatureService featureService, IApplicationCacheService applicationCacheService, IOrganizationUserRepository organizationUserRepository, IOrganizationRepository organizationRepository, IDataProtectorTokenFactory ssoEmail2faSessionTokeFactory, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, ICurrentContext currentContext) : ITwoFactorAuthenticationValidator { private readonly IUserService _userService = userService; private readonly UserManager _userManager = userManager; private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider = organizationDuoWebTokenProvider; - private readonly IFeatureService _featureService = featureService; private readonly IApplicationCacheService _applicationCacheService = applicationCacheService; private readonly IOrganizationUserRepository _organizationUserRepository = organizationUserRepository; private readonly IOrganizationRepository _organizationRepository = organizationRepository; private readonly IDataProtectorTokenFactory _ssoEmail2faSessionTokeFactory = ssoEmail2faSessionTokeFactory; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; private readonly ICurrentContext _currentContext = currentContext; public async Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request) @@ -161,7 +131,7 @@ public class TwoFactorAuthenticationValidator( // These cases we want to always return false, U2f is deprecated and OrganizationDuo // uses a different flow than the other two factor providers, it follows the same - // structure of a UserTokenProvider but has it's logic ran outside the usual token + // structure of a UserTokenProvider but has it's logic runs outside the usual token // provider flow. See IOrganizationDuoUniversalTokenProvider.cs if (type is TwoFactorProviderType.U2f or TwoFactorProviderType.OrganizationDuo) { @@ -171,12 +141,12 @@ public class TwoFactorAuthenticationValidator( // Now we are concerning the rest of the Two Factor Provider Types // The intent of this check is to make sure that the user is using a 2FA provider that - // is enabled and allowed by their premium status. The exception for Remember - // is because it is a "special" 2FA type that isn't ever explicitly + // is enabled and allowed by their premium status. + // The exception for Remember is because it is a "special" 2FA type that isn't ever explicitly // enabled by a user, so we can't check the user's 2FA providers to see if they're // enabled. We just have to check if the token is valid. if (type != TwoFactorProviderType.Remember && - !await _userService.TwoFactorProviderIsEnabledAsync(type, user)) + user.GetTwoFactorProvider(type) == null) { return false; } diff --git a/src/Infrastructure.Dapper/Repositories/UserRepository.cs b/src/Infrastructure.Dapper/Repositories/UserRepository.cs index 28478a0c41..6b11d64cda 100644 --- a/src/Infrastructure.Dapper/Repositories/UserRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/UserRepository.cs @@ -253,7 +253,6 @@ public class UserRepository : Repository, IUserRepository } } - public async Task UpdateUserKeyAndEncryptedDataV2Async( User user, IEnumerable updateDataActions) @@ -289,7 +288,6 @@ public class UserRepository : Repository, IUserRepository UnprotectData(user); } - public async Task> GetManyAsync(IEnumerable ids) { using (var connection = new SqlConnection(ReadOnlyConnectionString)) @@ -318,6 +316,14 @@ public class UserRepository : Repository, IUserRepository } } + public async Task GetCalculatedPremiumAsync(Guid userId) + { + var result = await GetManyWithCalculatedPremiumAsync([userId]); + + UnprotectData(result); + return result.SingleOrDefault(); + } + private async Task ProtectDataAndSaveAsync(User user, Func saveTask) { if (user == null) diff --git a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs index 127646ed59..bd70e27e78 100644 --- a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs @@ -1,10 +1,10 @@ using AutoMapper; using Bit.Core.KeyManagement.UserKey; +using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Infrastructure.EntityFramework.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using DataModel = Bit.Core.Models.Data; #nullable enable @@ -38,13 +38,13 @@ public class UserRepository : Repository, IUserR } } - public async Task GetKdfInformationByEmailAsync(string email) + public async Task GetKdfInformationByEmailAsync(string email) { using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); return await GetDbSet(dbContext).Where(e => e.Email == email) - .Select(e => new DataModel.UserKdfInformation + .Select(e => new UserKdfInformation { Kdf = e.Kdf, KdfIterations = e.KdfIterations, @@ -251,13 +251,13 @@ public class UserRepository : Repository, IUserR } } - public async Task> GetManyWithCalculatedPremiumAsync(IEnumerable ids) + public async Task> GetManyWithCalculatedPremiumAsync(IEnumerable ids) { using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); var users = dbContext.Users.Where(x => ids.Contains(x.Id)); - return await users.Select(e => new DataModel.UserWithCalculatedPremium(e) + return await users.Select(e => new UserWithCalculatedPremium(e) { HasPremiumAccess = e.Premium || dbContext.OrganizationUsers .Any(ou => ou.UserId == e.Id && @@ -269,6 +269,12 @@ public class UserRepository : Repository, IUserR } } + public async Task GetCalculatedPremiumAsync(Guid id) + { + var result = await GetManyWithCalculatedPremiumAsync([id]); + return result.FirstOrDefault(); + } + public override async Task DeleteAsync(Core.Entities.User user) { using (var scope = ServiceScopeFactory.CreateScope()) diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index bd22fd9346..c617f6c9a9 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -14,6 +14,7 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Entities; using Bit.Core.Exceptions; @@ -40,6 +41,7 @@ public class AccountsControllerTests : IDisposable private readonly IPolicyService _policyService; private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand; private readonly IRotateUserKeyCommand _rotateUserKeyCommand; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand; private readonly IFeatureService _featureService; @@ -64,6 +66,7 @@ public class AccountsControllerTests : IDisposable _policyService = Substitute.For(); _setInitialMasterPasswordCommand = Substitute.For(); _rotateUserKeyCommand = Substitute.For(); + _twoFactorIsEnabledQuery = Substitute.For(); _tdeOffboardingPasswordCommand = Substitute.For(); _featureService = Substitute.For(); _cipherValidator = @@ -87,6 +90,7 @@ public class AccountsControllerTests : IDisposable _setInitialMasterPasswordCommand, _tdeOffboardingPasswordCommand, _rotateUserKeyCommand, + _twoFactorIsEnabledQuery, _featureService, _cipherValidator, _folderValidator, diff --git a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs index 03c05ef0f4..ebbfc2a2ba 100644 --- a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs @@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Models; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -64,6 +65,7 @@ public class SyncControllerTests { // Get dependencies var userService = sutProvider.GetDependency(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); var organizationUserRepository = sutProvider.GetDependency(); var providerUserRepository = sutProvider.GetDependency(); var folderRepository = sutProvider.GetDependency(); @@ -119,7 +121,7 @@ public class SyncControllerTests collectionRepository.GetManyByUserIdAsync(user.Id).Returns(collections); collectionCipherRepository.GetManyByUserIdAsync(user.Id).Returns(new List()); // Back to standard test setup - userService.TwoFactorIsEnabledAsync(user).Returns(false); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false); userService.HasPremiumFromOrganization(user).Returns(false); // Execute GET @@ -129,7 +131,7 @@ public class SyncControllerTests // Asserts // Assert that methods are called var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled); - await this.AssertMethodsCalledAsync(userService, organizationUserRepository, providerUserRepository, folderRepository, + await this.AssertMethodsCalledAsync(userService, twoFactorIsEnabledQuery, organizationUserRepository, providerUserRepository, folderRepository, cipherRepository, sendRepository, collectionRepository, collectionCipherRepository, hasEnabledOrgs); Assert.IsType(result); @@ -155,6 +157,7 @@ public class SyncControllerTests { // Get dependencies var userService = sutProvider.GetDependency(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); var organizationUserRepository = sutProvider.GetDependency(); var providerUserRepository = sutProvider.GetDependency(); var folderRepository = sutProvider.GetDependency(); @@ -205,7 +208,7 @@ public class SyncControllerTests policyRepository.GetManyByUserIdAsync(user.Id).Returns(policies); - userService.TwoFactorIsEnabledAsync(user).Returns(false); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false); userService.HasPremiumFromOrganization(user).Returns(false); // Execute GET @@ -216,7 +219,7 @@ public class SyncControllerTests // Assert that methods are called var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled); - await this.AssertMethodsCalledAsync(userService, organizationUserRepository, providerUserRepository, folderRepository, + await this.AssertMethodsCalledAsync(userService, twoFactorIsEnabledQuery, organizationUserRepository, providerUserRepository, folderRepository, cipherRepository, sendRepository, collectionRepository, collectionCipherRepository, hasEnabledOrgs); Assert.IsType(result); @@ -244,6 +247,7 @@ public class SyncControllerTests { // Get dependencies var userService = sutProvider.GetDependency(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); var organizationUserRepository = sutProvider.GetDependency(); var providerUserRepository = sutProvider.GetDependency(); var folderRepository = sutProvider.GetDependency(); @@ -283,7 +287,7 @@ public class SyncControllerTests collectionRepository.GetManyByUserIdAsync(user.Id).Returns(collections); collectionCipherRepository.GetManyByUserIdAsync(user.Id).Returns(new List()); // Back to standard test setup - userService.TwoFactorIsEnabledAsync(user).Returns(false); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false); userService.HasPremiumFromOrganization(user).Returns(false); // Execute GET @@ -293,7 +297,7 @@ public class SyncControllerTests // Assert that methods are called var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled); - await this.AssertMethodsCalledAsync(userService, organizationUserRepository, providerUserRepository, folderRepository, + await this.AssertMethodsCalledAsync(userService, twoFactorIsEnabledQuery, organizationUserRepository, providerUserRepository, folderRepository, cipherRepository, sendRepository, collectionRepository, collectionCipherRepository, hasEnabledOrgs); Assert.IsType(result); @@ -315,6 +319,7 @@ public class SyncControllerTests private async Task AssertMethodsCalledAsync(IUserService userService, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IOrganizationUserRepository organizationUserRepository, IProviderUserRepository providerUserRepository, IFolderRepository folderRepository, ICipherRepository cipherRepository, ISendRepository sendRepository, @@ -356,7 +361,7 @@ public class SyncControllerTests .GetManyByUserIdAsync(default); } - await userService.ReceivedWithAnyArgs(1) + await twoFactorIsEnabledQuery.ReceivedWithAnyArgs(1) .TwoFactorIsEnabledAsync(default(ITwoFactorProvidersUser)); await userService.ReceivedWithAnyArgs(1) .HasPremiumFromOrganization(default); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs index 2dda23481a..baf844acae 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs @@ -2,6 +2,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; @@ -28,6 +29,7 @@ namespace Bit.Core.Test.OrganizationFeatures.OrganizationUsers; public class AcceptOrgUserCommandTests { private readonly IUserService _userService = Substitute.For(); + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery = Substitute.For(); private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory = Substitute.For(); private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory(); @@ -165,7 +167,7 @@ public class AcceptOrgUserCommandTests SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); // User doesn't have 2FA enabled - _userService.TwoFactorIsEnabledAsync(user).Returns(false); + _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false); // Organization they are trying to join requires 2FA var twoFactorPolicy = new OrganizationUserPolicyDetails { OrganizationId = orgUser.OrganizationId }; @@ -646,7 +648,7 @@ public class AcceptOrgUserCommandTests .Returns(false); // User doesn't have 2FA enabled - _userService.TwoFactorIsEnabledAsync(user).Returns(false); + _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false); // Org does not require 2FA sutProvider.GetDependency().GetPoliciesApplicableToUserAsync(user.Id, diff --git a/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs b/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs index da2d4a282a..ff09e1f141 100644 --- a/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs @@ -44,9 +44,6 @@ public abstract class BaseTokenProviderTests protected virtual void SetupUserService(IUserService userService, User user) { - userService - .TwoFactorProviderIsEnabledAsync(TwoFactorProviderType, user) - .Returns(true); userService .CanAccessPremium(user) .Returns(true); @@ -85,8 +82,6 @@ public abstract class BaseTokenProviderTests var userManager = SubstituteUserManager(); MockDatabase(user, metaData); - AdditionalSetup(sutProvider, user); - var response = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(userManager, user); Assert.Equal(expectedResponse, response); } diff --git a/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs b/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs index 85c687119b..5715403974 100644 --- a/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs @@ -83,6 +83,7 @@ public class DuoUniversalTwoFactorTokenProviderTests : BaseTokenProviderTests sutProvider) { // Arrange + AdditionalSetup(sutProvider, user); user.Premium = true; user.PremiumExpirationDate = DateTime.UtcNow.AddDays(1); @@ -100,6 +101,8 @@ public class DuoUniversalTwoFactorTokenProviderTests : BaseTokenProviderTests sutProvider) { // Arrange + AdditionalSetup(sutProvider, user); + user.Premium = false; sutProvider.GetDependency() diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQueryTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQueryTests.cs index 8011c52ead..adeac45d06 100644 --- a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQueryTests.cs +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQueryTests.cs @@ -5,6 +5,7 @@ using Bit.Core.Entities; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; +using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -53,6 +54,39 @@ public class TwoFactorIsEnabledQueryTests } } + [Theory, BitAutoData] + public async Task TwoFactorIsEnabledQuery_DatabaseReturnsEmpty_ResultEmpty( + SutProvider sutProvider, + List usersWithCalculatedPremium) + { + // Arrange + var userIds = usersWithCalculatedPremium.Select(u => u.Id).ToList(); + + sutProvider.GetDependency() + .GetManyWithCalculatedPremiumAsync(Arg.Any>()) + .Returns([]); + + // Act + var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds); + + // Assert + Assert.Empty(result); + } + + [Theory] + [BitAutoData((IEnumerable)null)] + [BitAutoData([])] + public async Task TwoFactorIsEnabledQuery_UserIdsNullorEmpty_ResultEmpty( + IEnumerable userIds, + SutProvider sutProvider) + { + // Act + var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds); + + // Assert + Assert.Empty(result); + } + [Theory] [BitAutoData] public async Task TwoFactorIsEnabledQuery_WithNoTwoFactorEnabled_ReturnsAllTwoFactorDisabled( @@ -122,8 +156,11 @@ public class TwoFactorIsEnabledQueryTests } [Theory] - [BitAutoData] - public async Task TwoFactorIsEnabledQuery_WithNullTwoFactorProviders_ReturnsAllTwoFactorDisabled( + [BitAutoData("")] + [BitAutoData("{}")] + [BitAutoData((string)null)] + public async Task TwoFactorIsEnabledQuery_WithNullOrEmptyTwoFactorProviders_ReturnsAllTwoFactorDisabled( + string twoFactorProviders, SutProvider sutProvider, List usersWithCalculatedPremium) { @@ -132,7 +169,7 @@ public class TwoFactorIsEnabledQueryTests foreach (var user in usersWithCalculatedPremium) { - user.TwoFactorProviders = null; // No two-factor providers configured + user.TwoFactorProviders = twoFactorProviders; // No two-factor providers configured } sutProvider.GetDependency() @@ -176,6 +213,24 @@ public class TwoFactorIsEnabledQueryTests .GetManyWithCalculatedPremiumAsync(default); } + [Theory] + [BitAutoData] + public async Task TwoFactorIsEnabledQuery_UserIdNull_ReturnsFalse( + SutProvider sutProvider) + { + // Arrange + var user = new TestTwoFactorProviderUser + { + Id = null + }; + + // Act + var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user); + + // Assert + Assert.False(result); + } + [Theory] [BitAutoData(TwoFactorProviderType.Authenticator)] [BitAutoData(TwoFactorProviderType.Email)] @@ -193,10 +248,8 @@ public class TwoFactorIsEnabledQueryTests { freeProviderType, new TwoFactorProvider { Enabled = true } } }; - user.Premium = false; user.SetTwoFactorProviders(twoFactorProviders); - // Act var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user); @@ -205,7 +258,7 @@ public class TwoFactorIsEnabledQueryTests await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .GetManyWithCalculatedPremiumAsync(default); + .GetCalculatedPremiumAsync(default); } [Theory] @@ -230,7 +283,7 @@ public class TwoFactorIsEnabledQueryTests await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .GetManyWithCalculatedPremiumAsync(default); + .GetCalculatedPremiumAsync(default); } [Theory] @@ -252,14 +305,18 @@ public class TwoFactorIsEnabledQueryTests user.SetTwoFactorProviders(twoFactorProviders); sutProvider.GetDependency() - .GetManyWithCalculatedPremiumAsync(Arg.Is>(i => i.Contains(user.Id))) - .Returns(new List { user }); + .GetCalculatedPremiumAsync(user.Id) + .Returns(user); // Act var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user); // Assert Assert.False(result); + + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(1) + .GetCalculatedPremiumAsync(default); } [Theory] @@ -268,7 +325,7 @@ public class TwoFactorIsEnabledQueryTests public async Task TwoFactorIsEnabledQuery_WithProviderTypeRequiringPremium_WithUserPremium_ReturnsTrue( TwoFactorProviderType premiumProviderType, SutProvider sutProvider, - User user) + UserWithCalculatedPremium user) { // Arrange var twoFactorProviders = new Dictionary @@ -276,9 +333,14 @@ public class TwoFactorIsEnabledQueryTests { premiumProviderType, new TwoFactorProvider { Enabled = true } } }; - user.Premium = true; + user.Premium = false; + user.HasPremiumAccess = true; user.SetTwoFactorProviders(twoFactorProviders); + sutProvider.GetDependency() + .GetCalculatedPremiumAsync(user.Id) + .Returns(user); + // Act var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user); @@ -286,8 +348,8 @@ public class TwoFactorIsEnabledQueryTests Assert.True(result); await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .GetManyWithCalculatedPremiumAsync(default); + .ReceivedWithAnyArgs(1) + .GetCalculatedPremiumAsync(default); } [Theory] @@ -309,14 +371,18 @@ public class TwoFactorIsEnabledQueryTests user.SetTwoFactorProviders(twoFactorProviders); sutProvider.GetDependency() - .GetManyWithCalculatedPremiumAsync(Arg.Is>(i => i.Contains(user.Id))) - .Returns(new List { user }); + .GetCalculatedPremiumAsync(user.Id) + .Returns(user); // Act var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user); // Assert Assert.True(result); + + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(1) + .GetCalculatedPremiumAsync(default); } [Theory] @@ -333,5 +399,29 @@ public class TwoFactorIsEnabledQueryTests // Assert Assert.False(result); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetCalculatedPremiumAsync(default); + } + + private class TestTwoFactorProviderUser : ITwoFactorProvidersUser + { + public Guid? Id { get; set; } + public string TwoFactorProviders { get; set; } + public bool Premium { get; set; } + public Dictionary GetTwoFactorProviders() + { + return JsonHelpers.LegacyDeserialize>(TwoFactorProviders); + } + + public Guid? GetUserId() + { + return Id; + } + + public bool GetPremium() + { + return Premium; + } } } diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index 02ff24d9bf..d9bb2beaca 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -9,6 +9,7 @@ using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; @@ -324,6 +325,7 @@ public class UserServiceTests sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), + sutProvider.GetDependency(), sutProvider.GetDependency() ); @@ -476,6 +478,9 @@ public class UserServiceTests sutProvider.GetDependency() .GetByIdAsync(organization.Id) .Returns(organization); + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(user) + .Returns(true); var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary { [TwoFactorProviderType.Remember] = new() { Enabled = true } @@ -911,6 +916,7 @@ public class UserServiceTests sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), + sutProvider.GetDependency(), sutProvider.GetDependency() ); } diff --git a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs index fb4d7c321a..1f075a0147 100644 --- a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs @@ -2,6 +2,7 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Models.Data.Organizations; @@ -27,11 +28,11 @@ public class TwoFactorAuthenticationValidatorTests private readonly IUserService _userService; private readonly UserManagerTestWrapper _userManager; private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider; - private readonly IFeatureService _featureService; private readonly IApplicationCacheService _applicationCacheService; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationRepository _organizationRepository; private readonly IDataProtectorTokenFactory _ssoEmail2faSessionTokenable; + private readonly ITwoFactorIsEnabledQuery _twoFactorenabledQuery; private readonly ICurrentContext _currentContext; private readonly TwoFactorAuthenticationValidator _sut; @@ -40,22 +41,22 @@ public class TwoFactorAuthenticationValidatorTests _userService = Substitute.For(); _userManager = SubstituteUserManager(); _organizationDuoUniversalTokenProvider = Substitute.For(); - _featureService = Substitute.For(); _applicationCacheService = Substitute.For(); _organizationUserRepository = Substitute.For(); _organizationRepository = Substitute.For(); _ssoEmail2faSessionTokenable = Substitute.For>(); + _twoFactorenabledQuery = Substitute.For(); _currentContext = Substitute.For(); _sut = new TwoFactorAuthenticationValidator( _userService, _userManager, _organizationDuoUniversalTokenProvider, - _featureService, _applicationCacheService, _organizationUserRepository, _organizationRepository, _ssoEmail2faSessionTokenable, + _twoFactorenabledQuery, _currentContext); } @@ -263,9 +264,6 @@ public class TwoFactorAuthenticationValidatorTests _userManager.SUPPORTS_TWO_FACTOR = true; _userManager.TWO_FACTOR_PROVIDERS = [providerType.ToString()]; - _userService.TwoFactorProviderIsEnabledAsync(Arg.Any(), user) - .Returns(true); - // Act var result = await _sut.BuildTwoFactorResultAsync(user, null); @@ -322,9 +320,6 @@ public class TwoFactorAuthenticationValidatorTests string token) { // Arrange - _userService.TwoFactorProviderIsEnabledAsync( - TwoFactorProviderType.Email, user).Returns(true); - _userManager.TWO_FACTOR_PROVIDERS = ["email"]; // Act @@ -342,10 +337,8 @@ public class TwoFactorAuthenticationValidatorTests string token) { // Arrange - _userService.TwoFactorProviderIsEnabledAsync( - TwoFactorProviderType.Email, user).Returns(false); - _userManager.TWO_FACTOR_PROVIDERS = ["email"]; + user.TwoFactorProviders = ""; // Act var result = await _sut.VerifyTwoFactorAsync( @@ -362,9 +355,6 @@ public class TwoFactorAuthenticationValidatorTests string token) { // Arrange - _userService.TwoFactorProviderIsEnabledAsync( - TwoFactorProviderType.OrganizationDuo, user).Returns(false); - _userManager.TWO_FACTOR_PROVIDERS = ["OrganizationDuo"]; // Act @@ -387,11 +377,9 @@ public class TwoFactorAuthenticationValidatorTests string token) { // Arrange - _userService.TwoFactorProviderIsEnabledAsync( - providerType, user).Returns(true); - _userManager.TWO_FACTOR_ENABLED = true; _userManager.TWO_FACTOR_TOKEN_VERIFIED = true; + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType); // Act var result = await _sut.VerifyTwoFactorAsync(user, null, providerType, token); @@ -412,11 +400,9 @@ public class TwoFactorAuthenticationValidatorTests string token) { // Arrange - _userService.TwoFactorProviderIsEnabledAsync( - providerType, user).Returns(true); - _userManager.TWO_FACTOR_ENABLED = true; _userManager.TWO_FACTOR_TOKEN_VERIFIED = false; + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType); // Act var result = await _sut.VerifyTwoFactorAsync(user, null, providerType, token); From ead5bbdd2a513e4627ad205577c2f83205b3d731 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Fri, 9 May 2025 12:13:01 -0400 Subject: [PATCH 16/20] [PM-21281] Email TOTP sent twice when user only has Email MFA enabled (#5782) * fix: addressed bug where email token is sent twice, * test: updating tests to have correct DI and removing test for automatic email of TOTP. --- .../Tokenables/SsoEmail2faSessionTokenable.cs | 7 ++++--- .../TwoFactorAuthenticationValidator.cs | 11 ++++------- .../TwoFactorAuthenticationValidatorTests.cs | 8 +++----- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Core/Auth/Models/Business/Tokenables/SsoEmail2faSessionTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/SsoEmail2faSessionTokenable.cs index 24a74bde07..30687a6a4a 100644 --- a/src/Core/Auth/Models/Business/Tokenables/SsoEmail2faSessionTokenable.cs +++ b/src/Core/Auth/Models/Business/Tokenables/SsoEmail2faSessionTokenable.cs @@ -4,9 +4,10 @@ using Bit.Core.Tokens; namespace Bit.Core.Auth.Models.Business.Tokenables; -// This token just provides a verifiable authN mechanism for the API service -// TwoFactorController.cs SendEmailLogin anonymous endpoint so it cannot be -// used maliciously. +/// +/// This token provides a verifiable authN mechanism for the TwoFactorController.SendEmailLoginAsync +/// anonymous endpoint so it cannot used maliciously. +/// public class SsoEmail2faSessionTokenable : ExpiringTokenable { // Just over 2 min expiration (client expires session after 2 min) diff --git a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs index 80b3b6e1f4..000f98c006 100644 --- a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs @@ -91,7 +91,10 @@ public class TwoFactorAuthenticationValidator( { "TwoFactorProviders2", providers }, }; - // If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token + // If we have an Email 2FA provider we need this session token so SSO users + // can re-request an email TOTP. The TwoFactorController.SendEmailLoginAsync + // endpoint requires a way to authenticate the user before sending another email with + // a TOTP, this token acts as the authentication mechanism. if (enabledProviders.Any(p => p.Key == TwoFactorProviderType.Email)) { twoFactorResultDict.Add("SsoEmail2faSessionToken", @@ -100,12 +103,6 @@ public class TwoFactorAuthenticationValidator( twoFactorResultDict.Add("Email", user.Email); } - if (enabledProviders.Count == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email) - { - // Send email now if this is their only 2FA method - await _userService.SendTwoFactorEmailAsync(user); - } - return twoFactorResultDict; } diff --git a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs index 1f075a0147..53e9a00c9f 100644 --- a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs @@ -252,9 +252,9 @@ public class TwoFactorAuthenticationValidatorTests [Theory] [BitAutoData(TwoFactorProviderType.Email)] - public async void BuildTwoFactorResultAsync_IndividualEmailProvider_SendsEmail_SetsSsoToken_ReturnsNotNull( - TwoFactorProviderType providerType, - User user) + public async void BuildTwoFactorResultAsync_SetsSsoToken_ReturnsNotNull( + TwoFactorProviderType providerType, + User user) { // Arrange var providerTypeInt = (int)providerType; @@ -276,8 +276,6 @@ public class TwoFactorAuthenticationValidatorTests Assert.True(providers.ContainsKey(providerTypeInt.ToString())); Assert.True(result.ContainsKey("SsoEmail2faSessionToken")); Assert.True(result.ContainsKey("Email")); - - await _userService.Received(1).SendTwoFactorEmailAsync(Arg.Any()); } [Theory] From 0075a15485daf3845c4dab42b7e3729850b131ad Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 9 May 2025 13:43:50 -0400 Subject: [PATCH 17/20] [PM-18064] Resolve billing warnings (#5797) * Resolve Billing warnings * Remove exclusions * Format --- src/Api/Api.csproj | 2 -- .../Controllers/OrganizationBillingController.cs | 2 ++ .../Billing/Services/IProviderBillingService.cs | 2 +- .../Implementations/PaymentHistoryService.cs | 4 +--- src/Core/Core.csproj | 2 -- .../Billing/Services/PaymentHistoryServiceTests.cs | 13 ++++--------- 6 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index 6505fdab5b..c490e90150 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -4,8 +4,6 @@ false bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml true - - $(WarningsNotAsErrors);CS8604 diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 2f0a4ef48b..b82c627ee0 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -1,4 +1,5 @@ #nullable enable +using System.Diagnostics; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; @@ -292,6 +293,7 @@ public class OrganizationBillingController( sale.SubscriptionSetup.SkipTrial = true; await organizationBillingService.Finalize(sale); var org = await organizationRepository.GetByIdAsync(organizationId); + Debug.Assert(org is not null, "This organization has already been found via this same ID, this should be fine."); if (organizationSignup.PaymentMethodType != null) { var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken); diff --git a/src/Core/Billing/Services/IProviderBillingService.cs b/src/Core/Billing/Services/IProviderBillingService.cs index 6ed8910dd8..0171a7e1c3 100644 --- a/src/Core/Billing/Services/IProviderBillingService.cs +++ b/src/Core/Billing/Services/IProviderBillingService.cs @@ -59,7 +59,7 @@ public interface IProviderBillingService int seatAdjustment); /// - /// Determines whether the provided will result in a purchase for the 's . + /// Determines whether the provided will result in a purchase for the 's . /// Seat adjustments that result in purchases include: /// /// The going from below the seat minimum to above the seat minimum for the provided diff --git a/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs b/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs index 6e984f946e..5a8cf16f5a 100644 --- a/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs +++ b/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs @@ -5,14 +5,12 @@ using Bit.Core.Entities; using Bit.Core.Models.BitStripe; using Bit.Core.Repositories; using Bit.Core.Services; -using Microsoft.Extensions.Logging; namespace Bit.Core.Billing.Services.Implementations; public class PaymentHistoryService( IStripeAdapter stripeAdapter, - ITransactionRepository transactionRepository, - ILogger logger) : IPaymentHistoryService + ITransactionRepository transactionRepository) : IPaymentHistoryService { public async Task> GetInvoiceHistoryAsync( ISubscriber subscriber, diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 4411a3de9b..898f0550b0 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -3,8 +3,6 @@ false bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml - - $(WarningsNotAsErrors);CS1574;CS9113 diff --git a/test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs b/test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs index c9278e4488..06a408c5a8 100644 --- a/test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs +++ b/test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs @@ -4,7 +4,6 @@ using Bit.Core.Entities; using Bit.Core.Models.BitStripe; using Bit.Core.Repositories; using Bit.Core.Services; -using Microsoft.Extensions.Logging; using NSubstitute; using Stripe; using Xunit; @@ -22,8 +21,7 @@ public class PaymentHistoryServiceTests var stripeAdapter = Substitute.For(); stripeAdapter.InvoiceListAsync(Arg.Any()).Returns(invoices); var transactionRepository = Substitute.For(); - var logger = Substitute.For>(); - var paymentHistoryService = new PaymentHistoryService(stripeAdapter, transactionRepository, logger); + var paymentHistoryService = new PaymentHistoryService(stripeAdapter, transactionRepository); // Act var result = await paymentHistoryService.GetInvoiceHistoryAsync(subscriber); @@ -40,8 +38,7 @@ public class PaymentHistoryServiceTests // Arrange var paymentHistoryService = new PaymentHistoryService( Substitute.For(), - Substitute.For(), - Substitute.For>()); + Substitute.For()); // Act var result = await paymentHistoryService.GetInvoiceHistoryAsync(null); @@ -59,8 +56,7 @@ public class PaymentHistoryServiceTests var transactionRepository = Substitute.For(); transactionRepository.GetManyByOrganizationIdAsync(subscriber.Id, Arg.Any(), Arg.Any()).Returns(transactions); var stripeAdapter = Substitute.For(); - var logger = Substitute.For>(); - var paymentHistoryService = new PaymentHistoryService(stripeAdapter, transactionRepository, logger); + var paymentHistoryService = new PaymentHistoryService(stripeAdapter, transactionRepository); // Act var result = await paymentHistoryService.GetTransactionHistoryAsync(subscriber); @@ -77,8 +73,7 @@ public class PaymentHistoryServiceTests // Arrange var paymentHistoryService = new PaymentHistoryService( Substitute.For(), - Substitute.For(), - Substitute.For>()); + Substitute.For()); // Act var result = await paymentHistoryService.GetTransactionHistoryAsync(null); From 15b498184f394e80ea1cbf8408941e89758e1919 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 9 May 2025 16:03:09 -0400 Subject: [PATCH 18/20] Resolve platform warnings (#5798) * Installation Repository tests * Formatting * Remove extra LastActivityDate property * Remove exclusion --- src/Core/Core.csproj | 4 ++ .../Infrastructure.EntityFramework.csproj | 5 -- .../Installations/Models/Installation.cs | 12 +---- .../InstallationRepositoryTests.cs | 46 +++++++++++++++++++ 4 files changed, 51 insertions(+), 16 deletions(-) create mode 100644 test/Infrastructure.IntegrationTest/Platform/Installations/InstallationRepositoryTests.cs diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 898f0550b0..6397e0b8ea 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -75,4 +75,8 @@ + + + + diff --git a/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj b/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj index 639d88524b..9814eef2aa 100644 --- a/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj +++ b/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj @@ -1,10 +1,5 @@ - - - $(WarningsNotAsErrors);CS0108 - - diff --git a/src/Infrastructure.EntityFramework/Platform/Installations/Models/Installation.cs b/src/Infrastructure.EntityFramework/Platform/Installations/Models/Installation.cs index 96b60a39ed..601ae993b3 100644 --- a/src/Infrastructure.EntityFramework/Platform/Installations/Models/Installation.cs +++ b/src/Infrastructure.EntityFramework/Platform/Installations/Models/Installation.cs @@ -3,22 +3,12 @@ using C = Bit.Core.Platform.Installations; namespace Bit.Infrastructure.EntityFramework.Platform; -public class Installation : C.Installation -{ - // Shadow property - to be introduced by https://bitwarden.atlassian.net/browse/PM-11129 - // This isn't a value or entity used by self hosted servers, but it's - // being added for synchronicity between database provider options. - public DateTime? LastActivityDate { get; set; } -} +public class Installation : C.Installation; public class InstallationMapperProfile : Profile { public InstallationMapperProfile() { - CreateMap() - // Shadow property - to be introduced by https://bitwarden.atlassian.net/browse/PM-11129 - .ForMember(i => i.LastActivityDate, opt => opt.Ignore()) - .ReverseMap(); CreateMap().ReverseMap(); } } diff --git a/test/Infrastructure.IntegrationTest/Platform/Installations/InstallationRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Platform/Installations/InstallationRepositoryTests.cs new file mode 100644 index 0000000000..2d212d4e39 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/Platform/Installations/InstallationRepositoryTests.cs @@ -0,0 +1,46 @@ +using Bit.Core.Platform.Installations; +using Bit.Infrastructure.IntegrationTest.Comparers; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.Platform.Installations; + +public class InstallationRepositoryTests +{ + [DatabaseTheory, DatabaseData] + public async Task GetByIdAsync_Works(IInstallationRepository installationRepository) + { + var installation = await installationRepository.CreateAsync(new Installation + { + Email = "test@email.com", + Key = "installation_key", + Enabled = true, + }); + + var retrievedInstallation = await installationRepository.GetByIdAsync(installation.Id); + + Assert.NotNull(retrievedInstallation); + Assert.Equal("installation_key", retrievedInstallation.Key); + } + + [DatabaseTheory, DatabaseData] + public async Task UpdateAsync_Works(IInstallationRepository installationRepository) + { + var installation = await installationRepository.CreateAsync(new Installation + { + Email = "test@email.com", + Key = "installation_key", + Enabled = true, + }); + + var now = DateTime.UtcNow; + + installation.LastActivityDate = now; + + await installationRepository.ReplaceAsync(installation); + + var retrievedInstallation = await installationRepository.GetByIdAsync(installation.Id); + + Assert.NotNull(retrievedInstallation.LastActivityDate); + Assert.Equal(now, retrievedInstallation.LastActivityDate.Value, LaxDateTimeComparer.Default); + } +} From 3a848d5747b59b4ffb78a82264dc527745c99705 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 12 May 2025 09:12:37 -0400 Subject: [PATCH 19/20] Move `Microsoft.Build.Sql` version to global.json (#5810) --- global.json | 3 ++- src/Sql/Sql.sqlproj | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/global.json b/global.json index 0c1d58f410..d04c13bbb5 100644 --- a/global.json +++ b/global.json @@ -4,6 +4,7 @@ "rollForward": "latestFeature" }, "msbuild-sdks": { - "Microsoft.Build.Traversal": "4.1.0" + "Microsoft.Build.Traversal": "4.1.0", + "Microsoft.Build.Sql": "0.1.9-preview" } } diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 65524fca45..849fd3bdfd 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -1,6 +1,6 @@  - + Sql {58554e52-fdec-4832-aff9-302b01e08dca} From e4359f071c89df5be7ceb31cb489d35bb199c8f9 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Mon, 12 May 2025 18:21:07 +0100 Subject: [PATCH 20/20] [PM-21097] Fix: Prevent admin-added sponsored families from appearing in individual vault settings (#5767) * Changes to resolve sponsorship showing in individual vault * Resolve the failing unit test Signed-off-by: Cy Okeke * Resolve the failing test * Resolve the failing test * Resolve the failing test * fix make IsAdminInitiated nullable Signed-off-by: Cy Okeke * Add the isAdminInitiated property Signed-off-by: Cy Okeke * Resolve the database error Signed-off-by: Cy Okeke * Resolve the failing unit test Signed-off-by: Cy Okeke * Resolve the scan error Signed-off-by: Cy Okeke * Resolve the database issue * resolve the database build error * Resolve the database build error * Resolve the synchronization issue --------- Signed-off-by: Cy Okeke --- .../ProfileOrganizationResponseModel.cs | 4 +- .../OrganizationSponsorshipsController.cs | 7 +- .../OrganizationUserOrganizationDetails.cs | 1 + .../OrganizationSponsorshipResponseModel.cs | 5 +- ...izationUserOrganizationDetailsViewQuery.cs | 6 +- ...ip_ReadBySponsoringOrganizationUserId.sql} | 9 +- ...rganizationUserOrganizationDetailsView.sql | 3 +- ...OrganizationSponsorshipsControllerTests.cs | 6 + .../OrganizationUserRepositoryTests.cs | 24 ++ ...-05_00_AddIsAdminInitiated_RefreshView.sql | 221 ++++++++++++++++++ ...hip_ReadBySponsoringOrganizationUserId.sql | 20 ++ 11 files changed, 294 insertions(+), 12 deletions(-) rename src/Sql/dbo/Stored Procedures/{OrganizationSponsorship_ReadBySponsoringOrganiationUserId.sql => OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql} (61%) create mode 100644 util/Migrator/DbScripts/2025-05-05_00_AddIsAdminInitiated_RefreshView.sql create mode 100644 util/Migrator/DbScripts/2025-05-05_01_AddIsAdminInitiated_OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql diff --git a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs index c74599a70e..5c900b73e0 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs @@ -58,7 +58,8 @@ public class ProfileOrganizationResponseModel : ResponseModel ProviderName = organization.ProviderName; ProviderType = organization.ProviderType; FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName; - FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null && + IsAdminInitiated = organization.IsAdminInitiated ?? false; + FamilySponsorshipAvailable = (FamilySponsorshipFriendlyName == null || IsAdminInitiated) && StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise) .UsersCanSponsor(organization); ProductTierType = organization.PlanType.GetProductTier(); @@ -157,4 +158,5 @@ public class ProfileOrganizationResponseModel : ResponseModel public bool UserIsClaimedByOrganization { get; set; } public bool UseRiskInsights { get; set; } public bool UseAdminSponsoredFamilies { get; set; } + public bool IsAdminInitiated { get; set; } } diff --git a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs index b007c05730..c4dc5fae75 100644 --- a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs @@ -271,8 +271,11 @@ public class OrganizationSponsorshipsController : Controller } var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId); - return new ListResponseModel(sponsorships.Select(s => - new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))); + return new ListResponseModel( + sponsorships + .Where(s => s.IsAdminInitiated) + .Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s))) + ); } diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs index 0771457d0a..a804dc0f6a 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs @@ -60,4 +60,5 @@ public class OrganizationUserOrganizationDetails public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool UseRiskInsights { get; set; } public bool UseAdminSponsoredFamilies { get; set; } + public bool? IsAdminInitiated { get; set; } } diff --git a/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs b/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs index 58c1b2cffb..e082d98de6 100644 --- a/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs +++ b/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs @@ -14,6 +14,7 @@ public class OrganizationSponsorshipResponseModel public bool ToDelete { get; set; } public bool CloudSponsorshipRemoved { get; set; } + public bool IsAdminInitiated { get; set; } public OrganizationSponsorshipResponseModel() { } @@ -27,6 +28,7 @@ public class OrganizationSponsorshipResponseModel ValidUntil = sponsorshipData.ValidUntil; ToDelete = sponsorshipData.ToDelete; CloudSponsorshipRemoved = sponsorshipData.CloudSponsorshipRemoved; + IsAdminInitiated = sponsorshipData.IsAdminInitiated; } public OrganizationSponsorshipData ToOrganizationSponsorship() @@ -40,7 +42,8 @@ public class OrganizationSponsorshipResponseModel LastSyncDate = LastSyncDate, ValidUntil = ValidUntil, ToDelete = ToDelete, - CloudSponsorshipRemoved = CloudSponsorshipRemoved + CloudSponsorshipRemoved = CloudSponsorshipRemoved, + IsAdminInitiated = IsAdminInitiated, }; } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs index 69f40bebb4..793abff8a2 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs @@ -7,8 +7,7 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery Run(DatabaseContext dbContext) { var query = from ou in dbContext.OrganizationUsers - join o in dbContext.Organizations on ou.OrganizationId equals o.Id into outerOrganization - from o in outerOrganization.DefaultIfEmpty() + join o in dbContext.Organizations on ou.OrganizationId equals o.Id join su in dbContext.SsoUsers on new { ou.UserId, OrganizationId = (Guid?)ou.OrganizationId } equals new { UserId = (Guid?)su.UserId, su.OrganizationId } into su_g from su in su_g.DefaultIfEmpty() join po in dbContext.ProviderOrganizations on o.Id equals po.OrganizationId into po_g @@ -68,10 +67,11 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery() .GetManyBySponsoringOrganizationAsync(sponsoringOrg.Id).Returns(sponsorships); + // Set IsAdminInitiated to true for all test sponsorships + foreach (var sponsorship in sponsorships) + { + sponsorship.IsAdminInitiated = true; + } + // Act var result = await sutProvider.Sut.GetSponsoredOrganizations(sponsoringOrg.Id); diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs index 637e970f8f..fd759e4777 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs @@ -316,6 +316,29 @@ public class OrganizationUserRepositoryTests 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 }); var organizationDomain = new OrganizationDomain @@ -335,6 +358,7 @@ public class OrganizationUserRepositoryTests UserId = user1.Id, Status = OrganizationUserStatusType.Confirmed, ResetPasswordKey = "resetpasswordkey1", + AccessSecretsManager = false }); await organizationUserRepository.CreateAsync(new OrganizationUser diff --git a/util/Migrator/DbScripts/2025-05-05_00_AddIsAdminInitiated_RefreshView.sql b/util/Migrator/DbScripts/2025-05-05_00_AddIsAdminInitiated_RefreshView.sql new file mode 100644 index 0000000000..8fd465025c --- /dev/null +++ b/util/Migrator/DbScripts/2025-05-05_00_AddIsAdminInitiated_RefreshView.sql @@ -0,0 +1,221 @@ +CREATE OR ALTER VIEW [dbo].[OrganizationUserOrganizationDetailsView] +AS +SELECT + OU.[UserId], + OU.[OrganizationId], + OU.[Id] OrganizationUserId, + O.[Name], + O.[Enabled], + O.[PlanType], + O.[UsePolicies], + O.[UseSso], + O.[UseKeyConnector], + O.[UseScim], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[SelfHost], + O.[UsersGetPremium], + O.[UseCustomPermissions], + O.[UseSecretsManager], + O.[Seats], + O.[MaxCollections], + O.[MaxStorageGb], + O.[Identifier], + OU.[Key], + OU.[ResetPasswordKey], + O.[PublicKey], + O.[PrivateKey], + OU.[Status], + OU.[Type], + SU.[ExternalId] SsoExternalId, + OU.[Permissions], + PO.[ProviderId], + P.[Name] ProviderName, + P.[Type] ProviderType, + SS.[Data] SsoConfig, + OS.[FriendlyName] FamilySponsorshipFriendlyName, + OS.[LastSyncDate] FamilySponsorshipLastSyncDate, + OS.[ToDelete] FamilySponsorshipToDelete, + OS.[ValidUntil] FamilySponsorshipValidUntil, + OU.[AccessSecretsManager], + O.[UsePasswordManager], + O.[SmSeats], + O.[SmServiceAccounts], + O.[LimitCollectionCreation], + O.[LimitCollectionDeletion], + O.[AllowAdminAccessToAllCollectionItems], + O.[UseRiskInsights], + O.[UseAdminSponsoredFamilies], + O.[LimitItemDeletion], + OS.[IsAdminInitiated] +FROM + [dbo].[OrganizationUser] OU + LEFT JOIN + [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] + LEFT JOIN + [dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId] + LEFT JOIN + [dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id] + LEFT JOIN + [dbo].[Provider] P ON P.[Id] = PO.[ProviderId] + LEFT JOIN + [dbo].[SsoConfig] SS ON SS.[OrganizationId] = OU.[OrganizationId] + LEFT JOIN + [dbo].[OrganizationSponsorship] OS ON OS.[SponsoringOrganizationUserID] = OU.[Id] +GO + +--Manually refresh [dbo].[OrganizationUserOrganizationDetailsView] +IF OBJECT_ID('[dbo].[OrganizationUserOrganizationDetailsView]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUserOrganizationDetailsView]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationView]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationView]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorshipView]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorshipView]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationUserOrganizationDetailsView]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUserOrganizationDetailsView]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_OrganizationUserDeleted]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_OrganizationUserDeleted]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_CreateMany]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_CreateMany]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_OrganizationUsersDeleted]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_OrganizationUsersDeleted]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_DeleteExpired]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_DeleteExpired]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_Update]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_Update]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_DeleteById]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_DeleteById]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_Create]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_Create]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_OrganizationDeleted]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_OrganizationDeleted]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_UpdateMany]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_UpdateMany]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_DeleteByIds]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_DeleteByIds]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadLatestBySponsoringOrganizationId]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_ReadLatestBySponsoringOrganizationId]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadByOfferedToEmail]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_ReadByOfferedToEmail]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadBySponsoredOrganizationId]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_ReadBySponsoredOrganizationId]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationId]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationId]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadById]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_ReadById]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatus]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatus]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatusOrganizationId]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatusOrganizationId]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationUser_DeleteById]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUser_DeleteById]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationUser_DeleteByIds]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUser_DeleteByIds]'; +END +GO + +IF OBJECT_ID('[dbo].[Organization_DeleteById]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[Organization_DeleteById]'; +END +GO diff --git a/util/Migrator/DbScripts/2025-05-05_01_AddIsAdminInitiated_OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql b/util/Migrator/DbScripts/2025-05-05_01_AddIsAdminInitiated_OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql new file mode 100644 index 0000000000..bb3bdee9b9 --- /dev/null +++ b/util/Migrator/DbScripts/2025-05-05_01_AddIsAdminInitiated_OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql @@ -0,0 +1,20 @@ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +ALTER PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId] + @SponsoringOrganizationUserId UNIQUEIDENTIFIER, + @IsAdminInitiated BIT = 0 +AS +BEGIN + SET NOCOUNT ON; + + SELECT + * + FROM + [dbo].[OrganizationSponsorshipView] + WHERE + [SponsoringOrganizationUserId] = @SponsoringOrganizationUserId + and [IsAdminInitiated] = @IsAdminInitiated +END +GO