diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6ab8d44d6..c6b0ecd9d0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,6 +11,9 @@ on: types: [opened, synchronize] workflow_call: inputs: {} + +permissions: + contents: read env: _AZ_REGISTRY: "bitwardenprod.azurecr.io" @@ -237,18 +240,10 @@ jobs: fi echo "tags=$TAGS" >> $GITHUB_OUTPUT - - name: Generate image full name - id: cache-name - env: - PROJECT_NAME: ${{ steps.setup.outputs.project_name }} - run: echo "name=${_AZ_REGISTRY}/${PROJECT_NAME}:buildcache" >> $GITHUB_OUTPUT - - name: Build Docker image id: build-artifacts uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0 with: - cache-from: type=registry,ref=${{ steps.cache-name.outputs.name }} - cache-to: type=registry,ref=${{ steps.cache-name.outputs.name}},mode=max context: . file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile platforms: | @@ -605,6 +600,7 @@ jobs: project: server pull_request_number: ${{ github.event.number }} secrets: inherit + permissions: read-all check-failures: name: Check for failures diff --git a/Directory.Build.props b/Directory.Build.props index e66c9c27c6..b369d6574d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.5.2 + 2025.6.0 Bit.$(MSBuildProjectName) enable @@ -69,4 +69,4 @@ - + \ No newline at end of file diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs index ad2d2d2aa1..3c75be756a 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs @@ -287,11 +287,10 @@ public class ProviderService : IProviderService foreach (var user in users) { - if (!keyedFilteredUsers.ContainsKey(user.Id)) + if (!keyedFilteredUsers.TryGetValue(user.Id, out var providerUser)) { continue; } - var providerUser = keyedFilteredUsers[user.Id]; try { if (providerUser.Status != ProviderUserStatusType.Accepted || providerUser.ProviderId != providerId) diff --git a/bitwarden_license/src/Scim/Program.cs b/bitwarden_license/src/Scim/Program.cs index 5d7d505aac..92f12f59dd 100644 --- a/bitwarden_license/src/Scim/Program.cs +++ b/bitwarden_license/src/Scim/Program.cs @@ -16,8 +16,8 @@ public class Program { var context = e.Properties["SourceContext"].ToString(); - if (e.Properties.ContainsKey("RequestPath") && - !string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) && + if (e.Properties.TryGetValue("RequestPath", out var requestPath) && + !string.IsNullOrWhiteSpace(requestPath?.ToString()) && (context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer"))) { return false; diff --git a/bitwarden_license/src/Sso/Controllers/AccountController.cs b/bitwarden_license/src/Sso/Controllers/AccountController.cs index f41d2d3c65..5c03ba0017 100644 --- a/bitwarden_license/src/Sso/Controllers/AccountController.cs +++ b/bitwarden_license/src/Sso/Controllers/AccountController.cs @@ -370,8 +370,8 @@ public class AccountController : Controller // for the user identifier. static bool nameIdIsNotTransient(Claim c) => c.Type == ClaimTypes.NameIdentifier && (c.Properties == null - || !c.Properties.ContainsKey(SamlPropertyKeys.ClaimFormat) - || c.Properties[SamlPropertyKeys.ClaimFormat] != SamlNameIdFormats.Transient); + || !c.Properties.TryGetValue(SamlPropertyKeys.ClaimFormat, out var claimFormat) + || claimFormat != SamlNameIdFormats.Transient); // Try to determine the unique id of the external user (issued by the provider) // the most common claim type for that are the sub claim and the NameIdentifier diff --git a/bitwarden_license/src/Sso/Program.cs b/bitwarden_license/src/Sso/Program.cs index 051caca9c2..1a8ce6eb88 100644 --- a/bitwarden_license/src/Sso/Program.cs +++ b/bitwarden_license/src/Sso/Program.cs @@ -17,8 +17,8 @@ public class Program logging.AddSerilog(hostingContext, (e, globalSettings) => { var context = e.Properties["SourceContext"].ToString(); - if (e.Properties.ContainsKey("RequestPath") && - !string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) && + if (e.Properties.TryGetValue("RequestPath", out var requestPath) && + !string.IsNullOrWhiteSpace(requestPath?.ToString()) && (context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer"))) { return false; diff --git a/bitwarden_license/src/Sso/Utilities/OpenIdConnectOptionsExtensions.cs b/bitwarden_license/src/Sso/Utilities/OpenIdConnectOptionsExtensions.cs index 9221877a04..825ed74dc8 100644 --- a/bitwarden_license/src/Sso/Utilities/OpenIdConnectOptionsExtensions.cs +++ b/bitwarden_license/src/Sso/Utilities/OpenIdConnectOptionsExtensions.cs @@ -46,9 +46,9 @@ public static class OpenIdConnectOptionsExtensions // Handle State if we've gotten that back var decodedState = options.StateDataFormat.Unprotect(state); - if (decodedState != null && decodedState.Items.ContainsKey("scheme")) + if (decodedState != null && decodedState.Items.TryGetValue("scheme", out var stateScheme)) { - return decodedState.Items["scheme"] == scheme; + return stateScheme == scheme; } } catch diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 601989a473..0ee4aa53a9 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -99,7 +99,7 @@ services: - idp rabbitmq: - image: rabbitmq:management + image: rabbitmq:4.1.0-management container_name: rabbitmq ports: - "5672:5672" @@ -108,7 +108,7 @@ services: RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER} RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS} volumes: - - rabbitmq_data:/var/lib/rabbitmq_data + - rabbitmq_data:/var/lib/rabbitmq profiles: - rabbitmq diff --git a/global.json b/global.json index d04c13bbb5..d25197db39 100644 --- a/global.json +++ b/global.json @@ -5,6 +5,6 @@ }, "msbuild-sdks": { "Microsoft.Build.Traversal": "4.1.0", - "Microsoft.Build.Sql": "0.1.9-preview" + "Microsoft.Build.Sql": "1.0.0" } } diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index 8cd2222dbf..6d38a77d8b 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -12,7 +12,6 @@ using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Providers.Services; -using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models.OrganizationConnectionConfigs; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; @@ -20,9 +19,6 @@ using Bit.Core.Repositories; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Bit.Core.Vault.Repositories; using Microsoft.AspNetCore.Authorization; @@ -45,12 +41,9 @@ public class OrganizationsController : Controller private readonly IPaymentService _paymentService; private readonly IApplicationCacheService _applicationCacheService; private readonly GlobalSettings _globalSettings; - private readonly IReferenceEventService _referenceEventService; - private readonly IUserService _userService; private readonly IProviderRepository _providerRepository; private readonly ILogger _logger; private readonly IAccessControlService _accessControlService; - private readonly ICurrentContext _currentContext; private readonly ISecretRepository _secretRepository; private readonly IProjectRepository _projectRepository; private readonly IServiceAccountRepository _serviceAccountRepository; @@ -73,12 +66,9 @@ public class OrganizationsController : Controller IPaymentService paymentService, IApplicationCacheService applicationCacheService, GlobalSettings globalSettings, - IReferenceEventService referenceEventService, - IUserService userService, IProviderRepository providerRepository, ILogger logger, IAccessControlService accessControlService, - ICurrentContext currentContext, ISecretRepository secretRepository, IProjectRepository projectRepository, IServiceAccountRepository serviceAccountRepository, @@ -100,12 +90,9 @@ public class OrganizationsController : Controller _paymentService = paymentService; _applicationCacheService = applicationCacheService; _globalSettings = globalSettings; - _referenceEventService = referenceEventService; - _userService = userService; _providerRepository = providerRepository; _logger = logger; _accessControlService = accessControlService; - _currentContext = currentContext; _secretRepository = secretRepository; _projectRepository = projectRepository; _serviceAccountRepository = serviceAccountRepository; @@ -272,11 +259,6 @@ public class OrganizationsController : Controller await _organizationRepository.ReplaceAsync(organization); await _applicationCacheService.UpsertOrganizationAbilityAsync(organization); - await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.OrganizationEditedByAdmin, organization, _currentContext) - { - EventRaisedByUser = _userService.GetUserName(User), - SalesAssistedTrialStarted = model.SalesAssistedTrialStarted, - }); return RedirectToAction("Edit", new { id }); } diff --git a/src/Admin/IdentityServer/ReadOnlyEnvIdentityUserStore.cs b/src/Admin/IdentityServer/ReadOnlyEnvIdentityUserStore.cs index 15b8d894b7..89f04230b3 100644 --- a/src/Admin/IdentityServer/ReadOnlyEnvIdentityUserStore.cs +++ b/src/Admin/IdentityServer/ReadOnlyEnvIdentityUserStore.cs @@ -39,7 +39,7 @@ public class ReadOnlyEnvIdentityUserStore : ReadOnlyIdentityUserStore } } - var userStamp = usersDict.ContainsKey(normalizedEmail) ? usersDict[normalizedEmail] : null; + var userStamp = usersDict.GetValueOrDefault(normalizedEmail); if (userStamp == null) { return Task.FromResult(null); diff --git a/src/Admin/Program.cs b/src/Admin/Program.cs index fb5dc7e08b..05bf35d41d 100644 --- a/src/Admin/Program.cs +++ b/src/Admin/Program.cs @@ -20,8 +20,8 @@ public class Program logging.AddSerilog(hostingContext, (e, globalSettings) => { var context = e.Properties["SourceContext"].ToString(); - if (e.Properties.ContainsKey("RequestPath") && - !string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) && + if (e.Properties.TryGetValue("RequestPath", out var requestPath) && + !string.IsNullOrWhiteSpace(requestPath?.ToString()) && (context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer"))) { return false; diff --git a/src/Admin/Services/AccessControlService.cs b/src/Admin/Services/AccessControlService.cs index f45f30e216..a2ba9fa6ff 100644 --- a/src/Admin/Services/AccessControlService.cs +++ b/src/Admin/Services/AccessControlService.cs @@ -29,12 +29,12 @@ public class AccessControlService : IAccessControlService } var userRole = GetUserRoleFromClaim(); - if (string.IsNullOrEmpty(userRole) || !RolePermissionMapping.RolePermissions.ContainsKey(userRole)) + if (string.IsNullOrEmpty(userRole) || !RolePermissionMapping.RolePermissions.TryGetValue(userRole, out var rolePermissions)) { return false; } - return RolePermissionMapping.RolePermissions[userRole].Contains(permission); + return rolePermissions.Contains(permission); } public string GetUserRole(string userEmail) diff --git a/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs b/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs index 357db5ad1e..8d7df4160d 100644 --- a/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs +++ b/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs @@ -25,7 +25,7 @@ public class UpdateTwoFactorAuthenticatorRequestModel : SecretVerificationReques { providers = new Dictionary(); } - else if (providers.ContainsKey(TwoFactorProviderType.Authenticator)) + else { providers.Remove(TwoFactorProviderType.Authenticator); } @@ -62,7 +62,7 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV { providers = []; } - else if (providers.ContainsKey(TwoFactorProviderType.Duo)) + else { providers.Remove(TwoFactorProviderType.Duo); } @@ -88,7 +88,7 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV { providers = []; } - else if (providers.ContainsKey(TwoFactorProviderType.OrganizationDuo)) + else { providers.Remove(TwoFactorProviderType.OrganizationDuo); } @@ -145,7 +145,7 @@ public class UpdateTwoFactorYubicoOtpRequestModel : SecretVerificationRequestMod { providers = new Dictionary(); } - else if (providers.ContainsKey(TwoFactorProviderType.YubiKey)) + else { providers.Remove(TwoFactorProviderType.YubiKey); } @@ -228,7 +228,7 @@ public class TwoFactorEmailRequestModel : SecretVerificationRequestModel { providers = new Dictionary(); } - else if (providers.ContainsKey(TwoFactorProviderType.Email)) + else { providers.Remove(TwoFactorProviderType.Email); } diff --git a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorAuthenticatorResponseModel.cs b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorAuthenticatorResponseModel.cs index f791c6fb1e..71569174a7 100644 --- a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorAuthenticatorResponseModel.cs +++ b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorAuthenticatorResponseModel.cs @@ -13,9 +13,9 @@ public class TwoFactorAuthenticatorResponseModel : ResponseModel ArgumentNullException.ThrowIfNull(user); var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator); - if (provider?.MetaData?.ContainsKey("Key") ?? false) + if (provider?.MetaData?.TryGetValue("Key", out var keyValue) ?? false) { - Key = (string)provider.MetaData["Key"]; + Key = (string)keyValue; Enabled = provider.Enabled; } else diff --git a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorEmailResponseModel.cs b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorEmailResponseModel.cs index ee1797f83e..d1d87d85b5 100644 --- a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorEmailResponseModel.cs +++ b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorEmailResponseModel.cs @@ -15,9 +15,9 @@ public class TwoFactorEmailResponseModel : ResponseModel } var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); - if (provider?.MetaData?.ContainsKey("Email") ?? false) + if (provider?.MetaData?.TryGetValue("Email", out var email) ?? false) { - Email = (string)provider.MetaData["Email"]; + Email = (string)email; Enabled = provider.Enabled; } else diff --git a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorYubiKeyResponseModel.cs b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorYubiKeyResponseModel.cs index 014863497d..0a97367017 100644 --- a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorYubiKeyResponseModel.cs +++ b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorYubiKeyResponseModel.cs @@ -19,29 +19,29 @@ public class TwoFactorYubiKeyResponseModel : ResponseModel { Enabled = provider.Enabled; - if (provider.MetaData.ContainsKey("Key1")) + if (provider.MetaData.TryGetValue("Key1", out var key1)) { - Key1 = (string)provider.MetaData["Key1"]; + Key1 = (string)key1; } - if (provider.MetaData.ContainsKey("Key2")) + if (provider.MetaData.TryGetValue("Key2", out var key2)) { - Key2 = (string)provider.MetaData["Key2"]; + Key2 = (string)key2; } - if (provider.MetaData.ContainsKey("Key3")) + if (provider.MetaData.TryGetValue("Key3", out var key3)) { - Key3 = (string)provider.MetaData["Key3"]; + Key3 = (string)key3; } - if (provider.MetaData.ContainsKey("Key4")) + if (provider.MetaData.TryGetValue("Key4", out var key4)) { - Key4 = (string)provider.MetaData["Key4"]; + Key4 = (string)key4; } - if (provider.MetaData.ContainsKey("Key5")) + if (provider.MetaData.TryGetValue("Key5", out var key5)) { - Key5 = (string)provider.MetaData["Key5"]; + Key5 = (string)key5; } - if (provider.MetaData.ContainsKey("Nfc")) + if (provider.MetaData.TryGetValue("Nfc", out var nfc)) { - Nfc = (bool)provider.MetaData["Nfc"]; + Nfc = (bool)nfc; } } else diff --git a/src/Api/Billing/Controllers/AccountsController.cs b/src/Api/Billing/Controllers/AccountsController.cs index 49ff679bb8..10d386641d 100644 --- a/src/Api/Billing/Controllers/AccountsController.cs +++ b/src/Api/Billing/Controllers/AccountsController.cs @@ -6,14 +6,10 @@ 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; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -161,8 +157,6 @@ public class AccountsController( [HttpPost("cancel")] public async Task PostCancelAsync( [FromBody] SubscriptionCancellationRequestModel request, - [FromServices] ICurrentContext currentContext, - [FromServices] IReferenceEventService referenceEventService, [FromServices] ISubscriberService subscriberService) { var user = await userService.GetUserByPrincipalAsync(User); @@ -175,12 +169,6 @@ public class AccountsController( await subscriberService.CancelSubscription(user, new OffboardingSurveyResponse { UserId = user.Id, Reason = request.Reason, Feedback = request.Feedback }, user.IsExpired()); - - await referenceEventService.RaiseEventAsync(new ReferenceEvent( - ReferenceEventType.CancelSubscription, - user, - currentContext) - { EndOfPeriod = user.IsExpired() }); } [HttpPost("reinstate-premium")] diff --git a/src/Api/Billing/Controllers/OrganizationsController.cs b/src/Api/Billing/Controllers/OrganizationsController.cs index bd5ab8cef4..c8a3c20c91 100644 --- a/src/Api/Billing/Controllers/OrganizationsController.cs +++ b/src/Api/Billing/Controllers/OrganizationsController.cs @@ -20,9 +20,6 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -44,7 +41,6 @@ public class OrganizationsController( IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand, IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand, - IReferenceEventService referenceEventService, ISubscriberService subscriberService, IOrganizationInstallationRepository organizationInstallationRepository, IPricingClient pricingClient) @@ -246,14 +242,6 @@ public class OrganizationsController( Feedback = request.Feedback }, organization.IsExpired()); - - await referenceEventService.RaiseEventAsync(new ReferenceEvent( - ReferenceEventType.CancelSubscription, - organization, - currentContext) - { - EndOfPeriod = organization.IsExpired() - }); } [HttpPost("{id:guid}/reinstate")] diff --git a/src/Api/Billing/Controllers/ProviderBillingController.cs b/src/Api/Billing/Controllers/ProviderBillingController.cs index 37130d54ce..1309b2df6d 100644 --- a/src/Api/Billing/Controllers/ProviderBillingController.cs +++ b/src/Api/Billing/Controllers/ProviderBillingController.cs @@ -81,13 +81,6 @@ public class ProviderBillingController( [FromRoute] Guid providerId, [FromBody] UpdatePaymentMethodRequestBody requestBody) { - var allowProviderPaymentMethod = featureService.IsEnabled(FeatureFlagKeys.PM18794_ProviderPaymentMethod); - - if (!allowProviderPaymentMethod) - { - return TypedResults.NotFound(); - } - var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId); if (provider == null) @@ -111,13 +104,6 @@ public class ProviderBillingController( [FromRoute] Guid providerId, [FromBody] VerifyBankAccountRequestBody requestBody) { - var allowProviderPaymentMethod = featureService.IsEnabled(FeatureFlagKeys.PM18794_ProviderPaymentMethod); - - if (!allowProviderPaymentMethod) - { - return TypedResults.NotFound(); - } - var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId); if (provider == null) diff --git a/src/Api/SecretsManager/Controllers/SecretsController.cs b/src/Api/SecretsManager/Controllers/SecretsController.cs index 9997e7502c..dd653bb873 100644 --- a/src/Api/SecretsManager/Controllers/SecretsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsController.cs @@ -5,7 +5,6 @@ using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Identity; -using Bit.Core.Repositories; using Bit.Core.SecretsManager.AuthorizationRequirements; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; using Bit.Core.SecretsManager.Entities; @@ -16,9 +15,6 @@ using Bit.Core.SecretsManager.Queries.Interfaces; using Bit.Core.SecretsManager.Queries.Secrets.Interfaces; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -30,7 +26,6 @@ public class SecretsController : Controller private readonly ICurrentContext _currentContext; private readonly IProjectRepository _projectRepository; private readonly ISecretRepository _secretRepository; - private readonly IOrganizationRepository _organizationRepository; private readonly ICreateSecretCommand _createSecretCommand; private readonly IUpdateSecretCommand _updateSecretCommand; private readonly IDeleteSecretCommand _deleteSecretCommand; @@ -39,14 +34,12 @@ public class SecretsController : Controller private readonly ISecretAccessPoliciesUpdatesQuery _secretAccessPoliciesUpdatesQuery; private readonly IUserService _userService; private readonly IEventService _eventService; - private readonly IReferenceEventService _referenceEventService; private readonly IAuthorizationService _authorizationService; public SecretsController( ICurrentContext currentContext, IProjectRepository projectRepository, ISecretRepository secretRepository, - IOrganizationRepository organizationRepository, ICreateSecretCommand createSecretCommand, IUpdateSecretCommand updateSecretCommand, IDeleteSecretCommand deleteSecretCommand, @@ -55,13 +48,11 @@ public class SecretsController : Controller ISecretAccessPoliciesUpdatesQuery secretAccessPoliciesUpdatesQuery, IUserService userService, IEventService eventService, - IReferenceEventService referenceEventService, IAuthorizationService authorizationService) { _currentContext = currentContext; _projectRepository = projectRepository; _secretRepository = secretRepository; - _organizationRepository = organizationRepository; _createSecretCommand = createSecretCommand; _updateSecretCommand = updateSecretCommand; _deleteSecretCommand = deleteSecretCommand; @@ -70,7 +61,6 @@ public class SecretsController : Controller _secretAccessPoliciesUpdatesQuery = secretAccessPoliciesUpdatesQuery; _userService = userService; _eventService = eventService; - _referenceEventService = referenceEventService; _authorizationService = authorizationService; } @@ -148,9 +138,6 @@ public class SecretsController : Controller if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount) { await _eventService.LogServiceAccountSecretEventAsync(userId, secret, EventType.Secret_Retrieved); - - var org = await _organizationRepository.GetByIdAsync(secret.OrganizationId); - await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.SmServiceAccountAccessedSecret, org, _currentContext)); } return new SecretResponseModel(secret, access.Read, access.Write); @@ -266,7 +253,7 @@ public class SecretsController : Controller throw new NotFoundException(); } - await LogSecretsRetrievalAsync(secrets.First().OrganizationId, secrets); + await LogSecretsRetrievalAsync(secrets); var responses = secrets.Select(s => new BaseSecretResponseModel(s)); return new ListResponseModel(responses); @@ -303,21 +290,18 @@ public class SecretsController : Controller if (syncResult.HasChanges) { - await LogSecretsRetrievalAsync(organizationId, syncResult.Secrets); + await LogSecretsRetrievalAsync(syncResult.Secrets); } return new SecretsSyncResponseModel(syncResult.HasChanges, syncResult.Secrets); } - private async Task LogSecretsRetrievalAsync(Guid organizationId, IEnumerable secrets) + private async Task LogSecretsRetrievalAsync(IEnumerable secrets) { if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount) { var userId = _userService.GetProperUserId(User)!.Value; - var org = await _organizationRepository.GetByIdAsync(organizationId); await _eventService.LogServiceAccountSecretsEventAsync(userId, secrets, EventType.Secret_Retrieved); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.SmServiceAccountAccessedSecret, org, _currentContext)); } } } diff --git a/src/Api/Tools/Controllers/SendsController.cs b/src/Api/Tools/Controllers/SendsController.cs index b18e603c0f..a51ec942cf 100644 --- a/src/Api/Tools/Controllers/SendsController.cs +++ b/src/Api/Tools/Controllers/SendsController.cs @@ -5,7 +5,6 @@ using Bit.Api.Tools.Models.Request; using Bit.Api.Tools.Models.Response; using Bit.Api.Utilities; using Bit.Core; -using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core.Settings; @@ -33,7 +32,6 @@ public class SendsController : Controller private readonly INonAnonymousSendCommand _nonAnonymousSendCommand; private readonly ILogger _logger; private readonly GlobalSettings _globalSettings; - private readonly ICurrentContext _currentContext; public SendsController( ISendRepository sendRepository, @@ -43,8 +41,7 @@ public class SendsController : Controller INonAnonymousSendCommand nonAnonymousSendCommand, ISendFileStorageService sendFileStorageService, ILogger logger, - GlobalSettings globalSettings, - ICurrentContext currentContext) + GlobalSettings globalSettings) { _sendRepository = sendRepository; _userService = userService; @@ -54,7 +51,6 @@ public class SendsController : Controller _sendFileStorageService = sendFileStorageService; _logger = logger; _globalSettings = globalSettings; - _currentContext = currentContext; } #region Anonymous endpoints diff --git a/src/Api/Utilities/ApiHelpers.cs b/src/Api/Utilities/ApiHelpers.cs index f4f1830e16..2c6dc8b73b 100644 --- a/src/Api/Utilities/ApiHelpers.cs +++ b/src/Api/Utilities/ApiHelpers.cs @@ -62,9 +62,9 @@ public static class ApiHelpers } } - if (eventTypeHandlers.ContainsKey(eventGridEvent.EventType)) + if (eventTypeHandlers.TryGetValue(eventGridEvent.EventType, out var eventTypeHandler)) { - await eventTypeHandlers[eventGridEvent.EventType](eventGridEvent); + await eventTypeHandler(eventGridEvent); } } diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 251362589e..7b302f3724 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -1064,7 +1064,7 @@ public class CiphersController : Controller [HttpPut("share")] [HttpPost("share")] - public async Task PutShareMany([FromBody] CipherBulkShareRequestModel model) + public async Task> PutShareMany([FromBody] CipherBulkShareRequestModel model) { var organizationId = new Guid(model.Ciphers.First().OrganizationId); if (!await _currentContext.OrganizationUser(organizationId)) @@ -1073,38 +1073,41 @@ public class CiphersController : Controller } var userId = _userService.GetProperUserId(User).Value; + var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false); var ciphersDict = ciphers.ToDictionary(c => c.Id); // Validate the model was encrypted for the posting user foreach (var cipher in model.Ciphers) { - if (cipher.EncryptedFor != null) + if (cipher.EncryptedFor.HasValue && cipher.EncryptedFor.Value != userId) { - if (cipher.EncryptedFor != userId) - { - throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); - } + throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); } } - var shareCiphers = new List<(Cipher, DateTime?)>(); + var shareCiphers = new List<(CipherDetails, DateTime?)>(); foreach (var cipher in model.Ciphers) { - if (!ciphersDict.ContainsKey(cipher.Id.Value)) + if (!ciphersDict.TryGetValue(cipher.Id.Value, out var existingCipher)) { - throw new BadRequestException("Trying to move ciphers that you do not own."); + throw new BadRequestException("Trying to share ciphers that you do not own."); } - var existingCipher = ciphersDict[cipher.Id.Value]; - ValidateClientVersionForFido2CredentialSupport(existingCipher); - shareCiphers.Add((cipher.ToCipher(existingCipher), cipher.LastKnownRevisionDate)); + shareCiphers.Add((cipher.ToCipherDetails(existingCipher), cipher.LastKnownRevisionDate)); } - await _cipherService.ShareManyAsync(shareCiphers, organizationId, - model.CollectionIds.Select(c => new Guid(c)), userId); + var updated = await _cipherService.ShareManyAsync( + shareCiphers, + organizationId, + model.CollectionIds.Select(Guid.Parse), + userId + ); + + var response = updated.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp)); + return new ListResponseModel(response); } [HttpPost("purge")] @@ -1186,14 +1189,14 @@ public class CiphersController : Controller var cipher = await GetByIdAsync(id, userId); var attachments = cipher?.GetAttachments(); - if (attachments == null || !attachments.ContainsKey(attachmentId) || attachments[attachmentId].Validated) + if (attachments == null || !attachments.TryGetValue(attachmentId, out var attachment) || attachment.Validated) { throw new NotFoundException(); } return new AttachmentUploadDataResponseModel { - Url = await _attachmentStorageService.GetAttachmentUploadUrlAsync(cipher, attachments[attachmentId]), + Url = await _attachmentStorageService.GetAttachmentUploadUrlAsync(cipher, attachment), FileUploadType = _attachmentStorageService.FileUploadType, }; } @@ -1212,11 +1215,10 @@ public class CiphersController : Controller var userId = _userService.GetProperUserId(User).Value; var cipher = await GetByIdAsync(id, userId); var attachments = cipher?.GetAttachments(); - if (attachments == null || !attachments.ContainsKey(attachmentId)) + if (attachments == null || !attachments.TryGetValue(attachmentId, out var attachmentData)) { throw new NotFoundException(); } - var attachmentData = attachments[attachmentId]; await Request.GetFileAsync(async (stream) => { @@ -1366,7 +1368,7 @@ public class CiphersController : Controller var cipher = await _cipherRepository.GetByIdAsync(new Guid(cipherId)); var attachments = cipher?.GetAttachments() ?? new Dictionary(); - if (cipher == null || !attachments.ContainsKey(attachmentId) || attachments[attachmentId].Validated) + if (cipher == null || !attachments.TryGetValue(attachmentId, out var attachment) || attachment.Validated) { if (_attachmentStorageService is AzureSendFileStorageService azureFileStorageService) { @@ -1376,7 +1378,7 @@ public class CiphersController : Controller return; } - await _cipherService.ValidateCipherAttachmentFile(cipher, attachments[attachmentId]); + await _cipherService.ValidateCipherAttachmentFile(cipher, attachment); } catch (Exception e) { diff --git a/src/Api/Vault/Models/Request/CipherRequestModel.cs b/src/Api/Vault/Models/Request/CipherRequestModel.cs index 5c288ab66d..229d27e484 100644 --- a/src/Api/Vault/Models/Request/CipherRequestModel.cs +++ b/src/Api/Vault/Models/Request/CipherRequestModel.cs @@ -113,18 +113,25 @@ public class CipherRequestModel if (hasAttachments2) { - foreach (var attachment in attachments.Where(a => Attachments2.ContainsKey(a.Key))) + foreach (var attachment in attachments) { - var attachment2 = Attachments2[attachment.Key]; + if (!Attachments2.TryGetValue(attachment.Key, out var attachment2)) + { + continue; + } attachment.Value.FileName = attachment2.FileName; attachment.Value.Key = attachment2.Key; } } else if (hasAttachments) { - foreach (var attachment in attachments.Where(a => Attachments.ContainsKey(a.Key))) + foreach (var attachment in attachments) { - attachment.Value.FileName = Attachments[attachment.Key]; + if (!Attachments.TryGetValue(attachment.Key, out var attachmentForKey)) + { + continue; + } + attachment.Value.FileName = attachmentForKey; attachment.Value.Key = null; } } diff --git a/src/Api/Vault/Models/Response/CipherResponseModel.cs b/src/Api/Vault/Models/Response/CipherResponseModel.cs index 358da3e62a..240783837e 100644 --- a/src/Api/Vault/Models/Response/CipherResponseModel.cs +++ b/src/Api/Vault/Models/Response/CipherResponseModel.cs @@ -129,13 +129,13 @@ public class CipherDetailsResponseModel : CipherResponseModel IDictionary> collectionCiphers, string obj = "cipherDetails") : base(cipher, user, organizationAbilities, globalSettings, obj) { - if (collectionCiphers?.ContainsKey(cipher.Id) ?? false) + if (collectionCiphers?.TryGetValue(cipher.Id, out var collectionCipher) ?? false) { - CollectionIds = collectionCiphers[cipher.Id].Select(c => c.CollectionId); + CollectionIds = collectionCipher.Select(c => c.CollectionId); } else { - CollectionIds = new Guid[] { }; + CollectionIds = []; } } @@ -147,7 +147,7 @@ public class CipherDetailsResponseModel : CipherResponseModel IEnumerable collectionCiphers, string obj = "cipherDetails") : base(cipher, user, organizationAbilities, globalSettings, obj) { - CollectionIds = collectionCiphers?.Select(c => c.CollectionId) ?? new List(); + CollectionIds = collectionCiphers?.Select(c => c.CollectionId) ?? []; } public CipherDetailsResponseModel( @@ -158,7 +158,7 @@ public class CipherDetailsResponseModel : CipherResponseModel string obj = "cipherDetails") : base(cipher, user, organizationAbilities, globalSettings, obj) { - CollectionIds = cipher.CollectionIds ?? new List(); + CollectionIds = cipher.CollectionIds ?? []; } public IEnumerable CollectionIds { get; set; } @@ -170,13 +170,13 @@ public class CipherMiniDetailsResponseModel : CipherMiniResponseModel IDictionary> collectionCiphers, bool orgUseTotp, string obj = "cipherMiniDetails") : base(cipher, globalSettings, orgUseTotp, obj) { - if (collectionCiphers?.ContainsKey(cipher.Id) ?? false) + if (collectionCiphers?.TryGetValue(cipher.Id, out var collectionCipher) ?? false) { - CollectionIds = collectionCiphers[cipher.Id].Select(c => c.CollectionId); + CollectionIds = collectionCipher.Select(c => c.CollectionId); } else { - CollectionIds = new Guid[] { }; + CollectionIds = []; } } @@ -184,7 +184,7 @@ public class CipherMiniDetailsResponseModel : CipherMiniResponseModel GlobalSettings globalSettings, bool orgUseTotp, string obj = "cipherMiniDetails") : base(cipher, globalSettings, orgUseTotp, obj) { - CollectionIds = cipher.CollectionIds ?? new List(); + CollectionIds = cipher.CollectionIds ?? []; } public CipherMiniDetailsResponseModel(CipherOrganizationDetailsWithCollections cipher, diff --git a/src/Billing/Controllers/AppleController.cs b/src/Billing/Controllers/AppleController.cs index 1bcbbf2ad6..5c231de8ed 100644 --- a/src/Billing/Controllers/AppleController.cs +++ b/src/Billing/Controllers/AppleController.cs @@ -28,8 +28,8 @@ public class AppleController : Controller return new BadRequestResult(); } - var key = HttpContext.Request.Query.ContainsKey("key") ? - HttpContext.Request.Query["key"].ToString() : null; + var key = HttpContext.Request.Query.TryGetValue("key", out var keyValue) ? + keyValue.ToString() : null; if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.AppleWebhookKey)) { return new BadRequestResult(); diff --git a/src/Billing/Controllers/PayPalController.cs b/src/Billing/Controllers/PayPalController.cs index 2afde80601..36987c6e44 100644 --- a/src/Billing/Controllers/PayPalController.cs +++ b/src/Billing/Controllers/PayPalController.cs @@ -51,8 +51,8 @@ public class PayPalController : Controller [HttpPost("ipn")] public async Task PostIpn() { - var key = HttpContext.Request.Query.ContainsKey("key") - ? HttpContext.Request.Query["key"].ToString() + var key = HttpContext.Request.Query.TryGetValue("key", out var keyValue) + ? keyValue.ToString() : null; if (string.IsNullOrEmpty(key)) diff --git a/src/Billing/Program.cs b/src/Billing/Program.cs index 33e2665427..3e005ce7fd 100644 --- a/src/Billing/Program.cs +++ b/src/Billing/Program.cs @@ -20,8 +20,8 @@ public class Program return e.Level >= globalSettings.MinLogLevel.BillingSettings.Jobs; } - if (e.Properties.ContainsKey("RequestPath") && - !string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) && + if (e.Properties.TryGetValue("RequestPath", out var requestPath) && + !string.IsNullOrWhiteSpace(requestPath?.ToString()) && (context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer"))) { return false; diff --git a/src/Billing/Services/Implementations/CustomerUpdatedHandler.cs b/src/Billing/Services/Implementations/CustomerUpdatedHandler.cs index 6deb0bc330..fe7745f760 100644 --- a/src/Billing/Services/Implementations/CustomerUpdatedHandler.cs +++ b/src/Billing/Services/Implementations/CustomerUpdatedHandler.cs @@ -1,8 +1,4 @@ -using Bit.Core.Context; -using Bit.Core.Repositories; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; +using Bit.Core.Repositories; using Event = Stripe.Event; namespace Bit.Billing.Services.Implementations; @@ -10,23 +6,17 @@ namespace Bit.Billing.Services.Implementations; public class CustomerUpdatedHandler : ICustomerUpdatedHandler { private readonly IOrganizationRepository _organizationRepository; - private readonly IReferenceEventService _referenceEventService; - private readonly ICurrentContext _currentContext; private readonly IStripeEventService _stripeEventService; private readonly IStripeEventUtilityService _stripeEventUtilityService; private readonly ILogger _logger; public CustomerUpdatedHandler( IOrganizationRepository organizationRepository, - IReferenceEventService referenceEventService, - ICurrentContext currentContext, IStripeEventService stripeEventService, IStripeEventUtilityService stripeEventUtilityService, ILogger logger) { _organizationRepository = organizationRepository ?? throw new ArgumentNullException(nameof(organizationRepository)); - _referenceEventService = referenceEventService; - _currentContext = currentContext; _stripeEventService = stripeEventService; _stripeEventUtilityService = stripeEventUtilityService; _logger = logger; @@ -95,20 +85,5 @@ public class CustomerUpdatedHandler : ICustomerUpdatedHandler organization.BillingEmail = customer.Email; await _organizationRepository.ReplaceAsync(organization); - - if (_referenceEventService == null) - { - _logger.LogError("ReferenceEventService was not initialized in CustomerUpdatedHandler"); - throw new InvalidOperationException($"{nameof(_referenceEventService)} is not initialized"); - } - - if (_currentContext == null) - { - _logger.LogError("CurrentContext was not initialized in CustomerUpdatedHandler"); - throw new InvalidOperationException($"{nameof(_currentContext)} is not initialized"); - } - - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.OrganizationEditedInStripe, organization, _currentContext)); } } diff --git a/src/Billing/Services/Implementations/PaymentSucceededHandler.cs b/src/Billing/Services/Implementations/PaymentSucceededHandler.cs index 40d8c8349d..4c256e3d85 100644 --- a/src/Billing/Services/Implementations/PaymentSucceededHandler.cs +++ b/src/Billing/Services/Implementations/PaymentSucceededHandler.cs @@ -3,13 +3,9 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; -using Bit.Core.Context; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Event = Stripe.Event; namespace Bit.Billing.Services.Implementations; @@ -22,9 +18,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler private readonly IStripeFacade _stripeFacade; private readonly IProviderRepository _providerRepository; private readonly IOrganizationRepository _organizationRepository; - private readonly IReferenceEventService _referenceEventService; - private readonly ICurrentContext _currentContext; - private readonly IUserRepository _userRepository; private readonly IStripeEventUtilityService _stripeEventUtilityService; private readonly IPushNotificationService _pushNotificationService; private readonly IOrganizationEnableCommand _organizationEnableCommand; @@ -36,9 +29,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler IStripeFacade stripeFacade, IProviderRepository providerRepository, IOrganizationRepository organizationRepository, - IReferenceEventService referenceEventService, - ICurrentContext currentContext, - IUserRepository userRepository, IStripeEventUtilityService stripeEventUtilityService, IUserService userService, IPushNotificationService pushNotificationService, @@ -50,9 +40,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler _stripeFacade = stripeFacade; _providerRepository = providerRepository; _organizationRepository = organizationRepository; - _referenceEventService = referenceEventService; - _currentContext = currentContext; - _userRepository = userRepository; _stripeEventUtilityService = stripeEventUtilityService; _userService = userService; _pushNotificationService = pushNotificationService; @@ -116,27 +103,7 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler _logger.LogError("invoice.payment_succeeded webhook ({EventID}) for Provider ({ProviderID}) indicates missing subscription line items", parsedEvent.Id, provider.Id); - - return; } - - await _referenceEventService.RaiseEventAsync(new ReferenceEvent - { - Type = ReferenceEventType.Rebilled, - Source = ReferenceEventSource.Provider, - Id = provider.Id, - PlanType = PlanType.TeamsMonthly, - Seats = (int)teamsMonthlyLineItem.Quantity - }); - - await _referenceEventService.RaiseEventAsync(new ReferenceEvent - { - Type = ReferenceEventType.Rebilled, - Source = ReferenceEventSource.Provider, - Id = provider.Id, - PlanType = PlanType.EnterpriseMonthly, - Seats = (int)enterpriseMonthlyLineItem.Quantity - }); } else if (organizationId.HasValue) { @@ -156,15 +123,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler await _organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd); await _pushNotificationService.PushSyncOrganizationStatusAsync(organization); - - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.Rebilled, organization, _currentContext) - { - PlanName = organization?.Plan, - PlanType = organization?.PlanType, - Seats = organization?.Seats, - Storage = organization?.MaxStorageGb, - }); } else if (userId.HasValue) { @@ -174,14 +132,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler } await _userService.EnablePremiumAsync(userId.Value, subscription.CurrentPeriodEnd); - - var user = await _userRepository.GetByIdAsync(userId.Value); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.Rebilled, user, _currentContext) - { - PlanName = IStripeEventUtilityService.PremiumPlanId, - Storage = user?.MaxStorageGb, - }); } } } diff --git a/src/Core/AdminConsole/Entities/Organization.cs b/src/Core/AdminConsole/Entities/Organization.cs index e649406bb0..274c7f8ddb 100644 --- a/src/Core/AdminConsole/Entities/Organization.cs +++ b/src/Core/AdminConsole/Entities/Organization.cs @@ -8,14 +8,13 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Services; -using Bit.Core.Tools.Entities; using Bit.Core.Utilities; #nullable enable namespace Bit.Core.AdminConsole.Entities; -public class Organization : ITableObject, IStorableSubscriber, IRevisable, IReferenceable +public class Organization : ITableObject, IStorableSubscriber, IRevisable { private Dictionary? _twoFactorProviders; @@ -258,12 +257,12 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable, public bool TwoFactorProviderIsEnabled(TwoFactorProviderType provider) { var providers = GetTwoFactorProviders(); - if (providers == null || !providers.ContainsKey(provider)) + if (providers == null || !providers.TryGetValue(provider, out var twoFactorProvider)) { return false; } - return providers[provider].Enabled && Use2fa; + return twoFactorProvider.Enabled && Use2fa; } public bool TwoFactorIsEnabled() @@ -280,12 +279,7 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable, public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider) { var providers = GetTwoFactorProviders(); - if (providers == null || !providers.ContainsKey(provider)) - { - return null; - } - - return providers[provider]; + return providers?.GetValueOrDefault(provider); } public void UpdateFromLicense(OrganizationLicense license, IFeatureService featureService) diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs b/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs index bd1f280cad..c94794765b 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs @@ -1,12 +1,15 @@ -using Bit.Core.Enums; +#nullable enable + +using Bit.Core.Enums; namespace Bit.Core.AdminConsole.Models.Data.Integrations; public interface IIntegrationMessage { IntegrationType IntegrationType { get; } - int RetryCount { get; set; } - DateTime? DelayUntilDate { get; set; } + string MessageId { get; set; } + int RetryCount { get; } + DateTime? DelayUntilDate { get; } void ApplyRetry(DateTime? handlerDelayUntilDate); string ToJson(); } diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs index d2f0bde693..ecf5d25c51 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs @@ -1,4 +1,6 @@ -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public class IntegrationHandlerResult { diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs index 1f288914d0..018d453cb9 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs @@ -1,13 +1,15 @@ -using System.Text.Json; +#nullable enable + +using System.Text.Json; using Bit.Core.Enums; namespace Bit.Core.AdminConsole.Models.Data.Integrations; -public class IntegrationMessage : IIntegrationMessage +public class IntegrationMessage : IIntegrationMessage { public IntegrationType IntegrationType { get; set; } - public T Configuration { get; set; } - public string RenderedTemplate { get; set; } + public required string MessageId { get; set; } + public required string RenderedTemplate { get; set; } public int RetryCount { get; set; } = 0; public DateTime? DelayUntilDate { get; set; } @@ -22,12 +24,22 @@ public class IntegrationMessage : IIntegrationMessage DelayUntilDate = baseTime.AddSeconds(backoffSeconds + jitterSeconds); } - public string ToJson() + public virtual string ToJson() + { + return JsonSerializer.Serialize(this); + } +} + +public class IntegrationMessage : IntegrationMessage +{ + public required T Configuration { get; set; } + + public override string ToJson() { return JsonSerializer.Serialize(this); } - public static IntegrationMessage FromJson(string json) + public static IntegrationMessage? FromJson(string json) { return JsonSerializer.Deserialize>(json); } diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs index 4fcce542ce..4f2c434ff6 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs @@ -1,3 +1,5 @@ -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record SlackIntegration(string token); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs index 2930004cbf..18b13248ec 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs @@ -1,3 +1,5 @@ -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record SlackIntegrationConfiguration(string channelId); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs index b81e50d403..a9b4150419 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs @@ -1,3 +1,5 @@ -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record SlackIntegrationConfigurationDetails(string channelId, string token); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs index e8217d3ad3..47e014ee2a 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs @@ -1,3 +1,5 @@ -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record WebhookIntegrationConfiguration(string url); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs index e3e92c900f..c4c41db24f 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs @@ -1,3 +1,5 @@ -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record WebhookIntegrationConfigurationDetails(string url); diff --git a/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs b/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs index 59debed746..ede2123f7e 100644 --- a/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs +++ b/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs @@ -1,4 +1,5 @@ - +#nullable enable + using System.Text.Json.Serialization; namespace Bit.Core.Models.Slack; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Groups/CreateGroupCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Groups/CreateGroupCommand.cs index 11bf6d7f66..f514beed38 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Groups/CreateGroupCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Groups/CreateGroupCommand.cs @@ -1,15 +1,11 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; namespace Bit.Core.AdminConsole.OrganizationFeatures.Groups; @@ -18,21 +14,16 @@ public class CreateGroupCommand : ICreateGroupCommand private readonly IEventService _eventService; private readonly IGroupRepository _groupRepository; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IReferenceEventService _referenceEventService; - private readonly ICurrentContext _currentContext; public CreateGroupCommand( IEventService eventService, IGroupRepository groupRepository, - IOrganizationUserRepository organizationUserRepository, - IReferenceEventService referenceEventService, - ICurrentContext currentContext) + IOrganizationUserRepository organizationUserRepository + ) { _eventService = eventService; _groupRepository = groupRepository; _organizationUserRepository = organizationUserRepository; - _referenceEventService = referenceEventService; - _currentContext = currentContext; } public async Task CreateGroupAsync(Group group, Organization organization, @@ -77,8 +68,6 @@ public class CreateGroupCommand : ICreateGroupCommand { await _groupRepository.CreateAsync(group, collections); } - - await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.GroupCreated, organization, _currentContext)); } private async Task GroupRepositoryUpdateUsersAsync(Group group, IEnumerable userIds, diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedOrganizationUserAccountCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedOrganizationUserAccountCommand.cs index 49ddf0a548..60a1c8bfbf 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedOrganizationUserAccountCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedOrganizationUserAccountCommand.cs @@ -7,9 +7,6 @@ using Bit.Core.Exceptions; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; #nullable enable @@ -24,7 +21,6 @@ public class DeleteClaimedOrganizationUserAccountCommand : IDeleteClaimedOrganiz private readonly IUserRepository _userRepository; private readonly ICurrentContext _currentContext; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; - private readonly IReferenceEventService _referenceEventService; private readonly IPushNotificationService _pushService; private readonly IOrganizationRepository _organizationRepository; private readonly IProviderUserRepository _providerUserRepository; @@ -36,7 +32,6 @@ public class DeleteClaimedOrganizationUserAccountCommand : IDeleteClaimedOrganiz IUserRepository userRepository, ICurrentContext currentContext, IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, - IReferenceEventService referenceEventService, IPushNotificationService pushService, IOrganizationRepository organizationRepository, IProviderUserRepository providerUserRepository) @@ -48,7 +43,6 @@ public class DeleteClaimedOrganizationUserAccountCommand : IDeleteClaimedOrganiz _userRepository = userRepository; _currentContext = currentContext; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; - _referenceEventService = referenceEventService; _pushService = pushService; _organizationRepository = organizationRepository; _providerUserRepository = providerUserRepository; @@ -195,8 +189,6 @@ public class DeleteClaimedOrganizationUserAccountCommand : IDeleteClaimedOrganiz await _userRepository.DeleteManyAsync(users); foreach (var user in users) { - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.DeleteAccount, user, _currentContext)); await _pushService.PushLogOutAsync(user.Id); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs index 072bc5fc05..db5d011e1d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs @@ -9,15 +9,11 @@ using Bit.Core.AdminConsole.Repositories; 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.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Microsoft.Extensions.Logging; using OrganizationUserInvite = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite; @@ -28,8 +24,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService, IInviteUsersValidator inviteUsersValidator, IPaymentService paymentService, IOrganizationRepository organizationRepository, - IReferenceEventService referenceEventService, - ICurrentContext currentContext, IApplicationCacheService applicationCacheService, IMailService mailService, ILogger logger, @@ -121,8 +115,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService, await SendAdditionalEmailsAsync(validatedRequest, organization); await SendInvitesAsync(organizationUserToInviteEntities, organization); - - await PublishReferenceEventAsync(validatedRequest, organization); } catch (Exception ex) { @@ -190,14 +182,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService, } } - private async Task PublishReferenceEventAsync(Valid validatedResult, - Organization organization) => - await referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.InvitedUsers, organization, currentContext) - { - Users = validatedResult.Value.Invites.Length - }); - private async Task SendInvitesAsync(IEnumerable users, Organization organization) => await sendOrganizationInvitesCommand.SendInvitesAsync( new SendInvitesRequest( @@ -284,15 +268,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService, await organizationRepository.ReplaceAsync(organization); // could optimize this with only a property update await applicationCacheService.UpsertOrganizationAbilityAsync(organization); - - await referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.AdjustSeats, organization, currentContext) - { - PlanName = validatedResult.Value.InviteOrganization.Plan.Name, - PlanType = validatedResult.Value.InviteOrganization.Plan.Type, - Seats = validatedResult.Value.PasswordManagerSubscriptionUpdate.UpdatedSeatTotal, - PreviousSeats = validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats - }); } } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs index 7449628ed0..f26061cbd2 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs @@ -5,7 +5,6 @@ using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; -using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -15,9 +14,6 @@ using Bit.Core.Models.StaticStore; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; @@ -36,8 +32,6 @@ public class CloudOrganizationSignUpCommand( IOrganizationBillingService organizationBillingService, IPaymentService paymentService, IPolicyService policyService, - IReferenceEventService referenceEventService, - ICurrentContext currentContext, IOrganizationRepository organizationRepository, IOrganizationApiKeyRepository organizationApiKeyRepository, IApplicationCacheService applicationCacheService, @@ -132,17 +126,6 @@ public class CloudOrganizationSignUpCommand( var ownerId = signup.IsFromProvider ? default : signup.Owner.Id; var returnValue = await SignUpAsync(organization, ownerId, signup.OwnerKey, signup.CollectionName, true); - await referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.Signup, organization, currentContext) - { - PlanName = plan.Name, - PlanType = plan.Type, - Seats = returnValue.Item1.Seats, - SignupInitiationPath = signup.InitiationPath, - Storage = returnValue.Item1.MaxStorageGb, - // TODO: add reference events for SmSeats and Service Accounts - see AC-1481 - }); - return new SignUpOrganizationResponse(returnValue.organization, returnValue.organizationUser); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDeleteCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDeleteCommand.cs index 185d5c5ac0..6a81130402 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDeleteCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDeleteCommand.cs @@ -2,38 +2,28 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; -using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; public class OrganizationDeleteCommand : IOrganizationDeleteCommand { private readonly IApplicationCacheService _applicationCacheService; - private readonly ICurrentContext _currentContext; private readonly IOrganizationRepository _organizationRepository; private readonly IPaymentService _paymentService; - private readonly IReferenceEventService _referenceEventService; private readonly ISsoConfigRepository _ssoConfigRepository; public OrganizationDeleteCommand( IApplicationCacheService applicationCacheService, - ICurrentContext currentContext, IOrganizationRepository organizationRepository, IPaymentService paymentService, - IReferenceEventService referenceEventService, ISsoConfigRepository ssoConfigRepository) { _applicationCacheService = applicationCacheService; - _currentContext = currentContext; _organizationRepository = organizationRepository; _paymentService = paymentService; - _referenceEventService = referenceEventService; _ssoConfigRepository = ssoConfigRepository; } @@ -48,8 +38,6 @@ public class OrganizationDeleteCommand : IOrganizationDeleteCommand var eop = !organization.ExpirationDate.HasValue || organization.ExpirationDate.Value >= DateTime.UtcNow; await _paymentService.CancelSubscriptionAsync(organization, eop); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.DeleteAccount, organization, _currentContext)); } catch (GatewayException) { } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs index b8802ffd0c..c3e945b65f 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs @@ -8,9 +8,6 @@ using Bit.Core.Models.Business; using Bit.Core.Models.StaticStore; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; @@ -37,7 +34,6 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati private readonly ICurrentContext _currentContext; private readonly IPricingClient _pricingClient; - private readonly IReferenceEventService _referenceEventService; private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; private readonly IApplicationCacheService _applicationCacheService; @@ -46,7 +42,6 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati public ProviderClientOrganizationSignUpCommand( ICurrentContext currentContext, IPricingClient pricingClient, - IReferenceEventService referenceEventService, IOrganizationRepository organizationRepository, IOrganizationApiKeyRepository organizationApiKeyRepository, IApplicationCacheService applicationCacheService, @@ -54,7 +49,6 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati { _currentContext = currentContext; _pricingClient = pricingClient; - _referenceEventService = referenceEventService; _organizationRepository = organizationRepository; _organizationApiKeyRepository = organizationApiKeyRepository; _applicationCacheService = applicationCacheService; @@ -108,16 +102,6 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati var returnValue = await SignUpAsync(organization, signup.CollectionName); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext) - { - PlanName = plan.Name, - PlanType = plan.Type, - Seats = returnValue.Organization.Seats, - SignupInitiationPath = signup.InitiationPath, - Storage = returnValue.Organization.MaxStorageGb, - }); - return returnValue; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs index cf332e689a..71212aaf4c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs @@ -104,8 +104,8 @@ public class SavePolicyCommand : ISavePolicyCommand var dependentPolicyTypes = _policyValidators.Values .Where(otherValidator => otherValidator.RequiredPolicies.Contains(policyUpdate.Type)) .Select(otherValidator => otherValidator.Type) - .Where(otherPolicyType => savedPoliciesDict.ContainsKey(otherPolicyType) && - savedPoliciesDict[otherPolicyType].Enabled) + .Where(otherPolicyType => savedPoliciesDict.TryGetValue(otherPolicyType, out var savedPolicy) && + savedPolicy.Enabled) .ToList(); switch (dependentPolicyTypes) diff --git a/src/Core/AdminConsole/Services/EventLoggingListenerService.cs b/src/Core/AdminConsole/Services/EventLoggingListenerService.cs index 60b8789a6b..ec2db121db 100644 --- a/src/Core/AdminConsole/Services/EventLoggingListenerService.cs +++ b/src/Core/AdminConsole/Services/EventLoggingListenerService.cs @@ -1,13 +1,87 @@ -using Microsoft.Extensions.Hosting; +#nullable enable + +using System.Text.Json; +using Bit.Core.Models.Data; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace Bit.Core.Services; public abstract class EventLoggingListenerService : BackgroundService { protected readonly IEventMessageHandler _handler; + protected ILogger _logger; - protected EventLoggingListenerService(IEventMessageHandler handler) + protected EventLoggingListenerService(IEventMessageHandler handler, ILogger logger) { - _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + _handler = handler; + _logger = logger; + } + + internal async Task ProcessReceivedMessageAsync(string body, string? messageId) + { + try + { + using var jsonDocument = JsonDocument.Parse(body); + var root = jsonDocument.RootElement; + + if (root.ValueKind == JsonValueKind.Array) + { + var eventMessages = root.Deserialize>(); + await _handler.HandleManyEventsAsync(eventMessages); + } + else if (root.ValueKind == JsonValueKind.Object) + { + var eventMessage = root.Deserialize(); + await _handler.HandleEventAsync(eventMessage); + } + else + { + if (!string.IsNullOrEmpty(messageId)) + { + _logger.LogError("An error occurred while processing message: {MessageId} - Invalid JSON", messageId); + } + else + { + _logger.LogError("An Invalid JSON error occurred while processing a message with an empty message id"); + } + } + } + catch (JsonException exception) + { + if (!string.IsNullOrEmpty(messageId)) + { + _logger.LogError( + exception, + "An error occurred while processing message: {MessageId} - Invalid JSON", + messageId + ); + } + else + { + _logger.LogError( + exception, + "An Invalid JSON error occurred while processing a message with an empty message id" + ); + } + } + catch (Exception exception) + { + if (!string.IsNullOrEmpty(messageId)) + { + _logger.LogError( + exception, + "An error occurred while processing message: {MessageId}", + messageId + ); + } + else + { + _logger.LogError( + exception, + "An error occurred while processing a message with an empty message id" + ); + } + } } } diff --git a/src/Core/AdminConsole/Services/IAzureServiceBusService.cs b/src/Core/AdminConsole/Services/IAzureServiceBusService.cs new file mode 100644 index 0000000000..d254e763d5 --- /dev/null +++ b/src/Core/AdminConsole/Services/IAzureServiceBusService.cs @@ -0,0 +1,10 @@ +using Azure.Messaging.ServiceBus; +using Bit.Core.AdminConsole.Models.Data.Integrations; + +namespace Bit.Core.Services; + +public interface IAzureServiceBusService : IEventIntegrationPublisher, IAsyncDisposable +{ + ServiceBusProcessor CreateProcessor(string topicName, string subscriptionName, ServiceBusProcessorOptions options); + Task PublishToRetryAsync(IIntegrationMessage message); +} diff --git a/src/Core/AdminConsole/Services/IIntegrationPublisher.cs b/src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs similarity index 58% rename from src/Core/AdminConsole/Services/IIntegrationPublisher.cs rename to src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs index 986ea776e1..560da576b7 100644 --- a/src/Core/AdminConsole/Services/IIntegrationPublisher.cs +++ b/src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs @@ -2,7 +2,8 @@ namespace Bit.Core.Services; -public interface IIntegrationPublisher +public interface IEventIntegrationPublisher : IAsyncDisposable { Task PublishAsync(IIntegrationMessage message); + Task PublishEventAsync(string body); } diff --git a/src/Core/AdminConsole/Services/IRabbitMqService.cs b/src/Core/AdminConsole/Services/IRabbitMqService.cs new file mode 100644 index 0000000000..b0b9a72eac --- /dev/null +++ b/src/Core/AdminConsole/Services/IRabbitMqService.cs @@ -0,0 +1,19 @@ +using Bit.Core.AdminConsole.Models.Data.Integrations; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace Bit.Core.Services; + +public interface IRabbitMqService : IEventIntegrationPublisher +{ + Task CreateChannelAsync(CancellationToken cancellationToken = default); + Task CreateEventQueueAsync(string queueName, CancellationToken cancellationToken = default); + Task CreateIntegrationQueuesAsync( + string queueName, + string retryQueueName, + string routingKey, + CancellationToken cancellationToken = default); + Task PublishToRetryAsync(IChannel channel, IIntegrationMessage message, CancellationToken cancellationToken); + Task PublishToDeadLetterAsync(IChannel channel, IIntegrationMessage message, CancellationToken cancellationToken); + Task RepublishToRetryQueueAsync(IChannel channel, BasicDeliverEventArgs eventArgs); +} diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs index 2ab10418a3..8b00204775 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs @@ -1,7 +1,7 @@ -using System.Text; -using System.Text.Json; +#nullable enable + +using System.Text; using Azure.Messaging.ServiceBus; -using Bit.Core.Models.Data; using Bit.Core.Settings; using Microsoft.Extensions.Logging; @@ -9,67 +9,47 @@ namespace Bit.Core.Services; public class AzureServiceBusEventListenerService : EventLoggingListenerService { - private readonly ILogger _logger; - private readonly ServiceBusClient _client; private readonly ServiceBusProcessor _processor; public AzureServiceBusEventListenerService( IEventMessageHandler handler, - ILogger logger, + IAzureServiceBusService serviceBusService, + string subscriptionName, GlobalSettings globalSettings, - string subscriptionName) : base(handler) + ILogger logger) : base(handler, logger) { - _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); - _processor = _client.CreateProcessor(globalSettings.EventLogging.AzureServiceBus.EventTopicName, subscriptionName, new ServiceBusProcessorOptions()); + _processor = serviceBusService.CreateProcessor( + globalSettings.EventLogging.AzureServiceBus.EventTopicName, + subscriptionName, + new ServiceBusProcessorOptions()); _logger = logger; } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { - _processor.ProcessMessageAsync += async args => - { - try - { - using var jsonDocument = JsonDocument.Parse(Encoding.UTF8.GetString(args.Message.Body)); - var root = jsonDocument.RootElement; - - if (root.ValueKind == JsonValueKind.Array) - { - var eventMessages = root.Deserialize>(); - await _handler.HandleManyEventsAsync(eventMessages); - } - else if (root.ValueKind == JsonValueKind.Object) - { - var eventMessage = root.Deserialize(); - await _handler.HandleEventAsync(eventMessage); - - } - await args.CompleteMessageAsync(args.Message); - } - catch (Exception exception) - { - _logger.LogError( - exception, - "An error occured while processing message: {MessageId}", - args.Message.MessageId - ); - } - }; - - _processor.ProcessErrorAsync += args => - { - _logger.LogError( - args.Exception, - "An error occurred. Entity Path: {EntityPath}, Error Source: {ErrorSource}", - args.EntityPath, - args.ErrorSource - ); - return Task.CompletedTask; - }; + _processor.ProcessMessageAsync += ProcessReceivedMessageAsync; + _processor.ProcessErrorAsync += ProcessErrorAsync; await _processor.StartProcessingAsync(cancellationToken); } + internal Task ProcessErrorAsync(ProcessErrorEventArgs args) + { + _logger.LogError( + args.Exception, + "An error occurred. Entity Path: {EntityPath}, Error Source: {ErrorSource}", + args.EntityPath, + args.ErrorSource + ); + return Task.CompletedTask; + } + + private async Task ProcessReceivedMessageAsync(ProcessMessageEventArgs args) + { + await ProcessReceivedMessageAsync(Encoding.UTF8.GetString(args.Message.Body), args.Message.MessageId); + await args.CompleteMessageAsync(args.Message); + } + public override async Task StopAsync(CancellationToken cancellationToken) { await _processor.StopProcessingAsync(cancellationToken); @@ -79,7 +59,6 @@ public class AzureServiceBusEventListenerService : EventLoggingListenerService public override void Dispose() { _processor.DisposeAsync().GetAwaiter().GetResult(); - _client.DisposeAsync().GetAwaiter().GetResult(); base.Dispose(); } } diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs deleted file mode 100644 index 224f86a802..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Text.Json; -using Azure.Messaging.ServiceBus; -using Bit.Core.Models.Data; -using Bit.Core.Services; -using Bit.Core.Settings; - -namespace Bit.Core.AdminConsole.Services.Implementations; - -public class AzureServiceBusEventWriteService : IEventWriteService, IAsyncDisposable -{ - private readonly ServiceBusClient _client; - private readonly ServiceBusSender _sender; - - public AzureServiceBusEventWriteService(GlobalSettings globalSettings) - { - _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); - _sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.EventTopicName); - } - - public async Task CreateAsync(IEvent e) - { - var message = new ServiceBusMessage(JsonSerializer.SerializeToUtf8Bytes(e)) - { - ContentType = "application/json" - }; - - await _sender.SendMessageAsync(message); - } - - public async Task CreateManyAsync(IEnumerable events) - { - var message = new ServiceBusMessage(JsonSerializer.SerializeToUtf8Bytes(events)) - { - ContentType = "application/json" - }; - - await _sender.SendMessageAsync(message); - } - - public async ValueTask DisposeAsync() - { - await _sender.DisposeAsync(); - await _client.DisposeAsync(); - } -} diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs index 8244f39c09..55a39ec774 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs @@ -1,7 +1,6 @@ #nullable enable using Azure.Messaging.ServiceBus; -using Bit.Core.Settings; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -10,39 +9,30 @@ namespace Bit.Core.Services; public class AzureServiceBusIntegrationListenerService : BackgroundService { private readonly int _maxRetries; - private readonly string _subscriptionName; - private readonly string _topicName; + private readonly IAzureServiceBusService _serviceBusService; private readonly IIntegrationHandler _handler; - private readonly ServiceBusClient _client; private readonly ServiceBusProcessor _processor; - private readonly ServiceBusSender _sender; private readonly ILogger _logger; - public AzureServiceBusIntegrationListenerService( - IIntegrationHandler handler, + public AzureServiceBusIntegrationListenerService(IIntegrationHandler handler, + string topicName, string subscriptionName, - GlobalSettings globalSettings, + int maxRetries, + IAzureServiceBusService serviceBusService, ILogger logger) { _handler = handler; _logger = logger; - _maxRetries = globalSettings.EventLogging.AzureServiceBus.MaxRetries; - _topicName = globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName; - _subscriptionName = subscriptionName; + _maxRetries = maxRetries; + _serviceBusService = serviceBusService; - _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); - _processor = _client.CreateProcessor(_topicName, _subscriptionName, new ServiceBusProcessorOptions()); - _sender = _client.CreateSender(_topicName); + _processor = _serviceBusService.CreateProcessor(topicName, subscriptionName, new ServiceBusProcessorOptions()); } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { _processor.ProcessMessageAsync += HandleMessageAsync; - _processor.ProcessErrorAsync += args => - { - _logger.LogError(args.Exception, "Azure Service Bus error"); - return Task.CompletedTask; - }; + _processor.ProcessErrorAsync += ProcessErrorAsync; await _processor.StartProcessingAsync(cancellationToken); } @@ -51,51 +41,67 @@ public class AzureServiceBusIntegrationListenerService : BackgroundService { await _processor.StopProcessingAsync(cancellationToken); await _processor.DisposeAsync(); - await _sender.DisposeAsync(); - await _client.DisposeAsync(); await base.StopAsync(cancellationToken); } - private async Task HandleMessageAsync(ProcessMessageEventArgs args) + internal Task ProcessErrorAsync(ProcessErrorEventArgs args) { - var json = args.Message.Body.ToString(); + _logger.LogError( + args.Exception, + "An error occurred. Entity Path: {EntityPath}, Error Source: {ErrorSource}", + args.EntityPath, + args.ErrorSource + ); + return Task.CompletedTask; + } + internal async Task HandleMessageAsync(string body) + { try { - var result = await _handler.HandleAsync(json); + var result = await _handler.HandleAsync(body); var message = result.Message; if (result.Success) { - await args.CompleteMessageAsync(args.Message); - return; + // Successful integration. Return true to indicate the message has been handled + return true; } message.ApplyRetry(result.DelayUntilDate); if (result.Retryable && message.RetryCount < _maxRetries) { - var scheduledTime = (DateTime)message.DelayUntilDate!; - var retryMsg = new ServiceBusMessage(message.ToJson()) - { - Subject = args.Message.Subject, - ScheduledEnqueueTime = scheduledTime - }; - - await _sender.SendMessageAsync(retryMsg); + // Publish message to the retry queue. It will be re-published for retry after a delay + // Return true to indicate the message has been handled + await _serviceBusService.PublishToRetryAsync(message); + return true; } else { - await args.DeadLetterMessageAsync(args.Message, "Retry limit exceeded or non-retryable"); - return; + // Non-recoverable failure or exceeded the max number of retries + // Return false to indicate this message should be dead-lettered + return false; } - - await args.CompleteMessageAsync(args.Message); } catch (Exception ex) { + // Unknown exception - log error, return true so the message will be acknowledged and not resent _logger.LogError(ex, "Unhandled error processing ASB message"); + return true; + } + } + + private async Task HandleMessageAsync(ProcessMessageEventArgs args) + { + var json = args.Message.Body.ToString(); + if (await HandleMessageAsync(json)) + { await args.CompleteMessageAsync(args.Message); } + else + { + await args.DeadLetterMessageAsync(args.Message, "Retry limit exceeded or non-retryable"); + } } } diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationPublisher.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationPublisher.cs deleted file mode 100644 index 4a906e719f..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationPublisher.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Azure.Messaging.ServiceBus; -using Bit.Core.AdminConsole.Models.Data.Integrations; -using Bit.Core.Enums; -using Bit.Core.Settings; - -namespace Bit.Core.Services; - -public class AzureServiceBusIntegrationPublisher : IIntegrationPublisher, IAsyncDisposable -{ - private readonly ServiceBusClient _client; - private readonly ServiceBusSender _sender; - - public AzureServiceBusIntegrationPublisher(GlobalSettings globalSettings) - { - _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); - _sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName); - } - - public async Task PublishAsync(IIntegrationMessage message) - { - var json = message.ToJson(); - - var serviceBusMessage = new ServiceBusMessage(json) - { - Subject = message.IntegrationType.ToRoutingKey(), - }; - - await _sender.SendMessageAsync(serviceBusMessage); - } - - public async ValueTask DisposeAsync() - { - await _sender.DisposeAsync(); - await _client.DisposeAsync(); - } -} diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusService.cs new file mode 100644 index 0000000000..7d24095819 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusService.cs @@ -0,0 +1,70 @@ +using Azure.Messaging.ServiceBus; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Enums; +using Bit.Core.Settings; + +namespace Bit.Core.Services; + +public class AzureServiceBusService : IAzureServiceBusService +{ + private readonly ServiceBusClient _client; + private readonly ServiceBusSender _eventSender; + private readonly ServiceBusSender _integrationSender; + + public AzureServiceBusService(GlobalSettings globalSettings) + { + _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); + _eventSender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.EventTopicName); + _integrationSender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName); + } + + public ServiceBusProcessor CreateProcessor(string topicName, string subscriptionName, ServiceBusProcessorOptions options) + { + return _client.CreateProcessor(topicName, subscriptionName, options); + } + + public async Task PublishAsync(IIntegrationMessage message) + { + var json = message.ToJson(); + + var serviceBusMessage = new ServiceBusMessage(json) + { + Subject = message.IntegrationType.ToRoutingKey(), + MessageId = message.MessageId + }; + + await _integrationSender.SendMessageAsync(serviceBusMessage); + } + + public async Task PublishToRetryAsync(IIntegrationMessage message) + { + var json = message.ToJson(); + + var serviceBusMessage = new ServiceBusMessage(json) + { + Subject = message.IntegrationType.ToRoutingKey(), + ScheduledEnqueueTime = message.DelayUntilDate ?? DateTime.UtcNow, + MessageId = message.MessageId + }; + + await _integrationSender.SendMessageAsync(serviceBusMessage); + } + + public async Task PublishEventAsync(string body) + { + var message = new ServiceBusMessage(body) + { + ContentType = "application/json", + MessageId = Guid.NewGuid().ToString() + }; + + await _eventSender.SendMessageAsync(message); + } + + public async ValueTask DisposeAsync() + { + await _eventSender.DisposeAsync(); + await _integrationSender.DisposeAsync(); + await _client.DisposeAsync(); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs index aa545913b1..578dde9485 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs @@ -1,4 +1,6 @@ -using Bit.Core.Models.Data; +#nullable enable + +using Bit.Core.Models.Data; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrationEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrationEventWriteService.cs new file mode 100644 index 0000000000..519f8aeb32 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrationEventWriteService.cs @@ -0,0 +1,32 @@ +#nullable enable + +using System.Text.Json; +using Bit.Core.Models.Data; + +namespace Bit.Core.Services; +public class EventIntegrationEventWriteService : IEventWriteService, IAsyncDisposable +{ + private readonly IEventIntegrationPublisher _eventIntegrationPublisher; + + public EventIntegrationEventWriteService(IEventIntegrationPublisher eventIntegrationPublisher) + { + _eventIntegrationPublisher = eventIntegrationPublisher; + } + + public async Task CreateAsync(IEvent e) + { + var body = JsonSerializer.Serialize(e); + await _eventIntegrationPublisher.PublishEventAsync(body: body); + } + + public async Task CreateManyAsync(IEnumerable events) + { + var body = JsonSerializer.Serialize(events); + await _eventIntegrationPublisher.PublishEventAsync(body: body); + } + + public async ValueTask DisposeAsync() + { + await _eventIntegrationPublisher.DisposeAsync(); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs index 9a80ed67b2..aa76fdf8bc 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs @@ -1,4 +1,6 @@ -using System.Text.Json; +#nullable enable + +using System.Text.Json; using Bit.Core.AdminConsole.Models.Data.Integrations; using Bit.Core.AdminConsole.Utilities; using Bit.Core.Enums; @@ -7,11 +9,9 @@ using Bit.Core.Repositories; namespace Bit.Core.Services; -#nullable enable - public class EventIntegrationHandler( IntegrationType integrationType, - IIntegrationPublisher integrationPublisher, + IEventIntegrationPublisher eventIntegrationPublisher, IOrganizationIntegrationConfigurationRepository configurationRepository, IUserRepository userRepository, IOrganizationRepository organizationRepository) @@ -34,6 +34,7 @@ public class EventIntegrationHandler( var template = configuration.Template ?? string.Empty; var context = await BuildContextAsync(eventMessage, template); var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(template, context); + var messageId = eventMessage.IdempotencyId ?? Guid.NewGuid(); var config = configuration.MergedConfiguration.Deserialize() ?? throw new InvalidOperationException($"Failed to deserialize to {typeof(T).Name}"); @@ -41,13 +42,14 @@ public class EventIntegrationHandler( var message = new IntegrationMessage { IntegrationType = integrationType, + MessageId = messageId.ToString(), Configuration = config, RenderedTemplate = renderedTemplate, RetryCount = 0, DelayUntilDate = null }; - await integrationPublisher.PublishAsync(message); + await eventIntegrationPublisher.PublishAsync(message); } } diff --git a/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs index ee3a2d5db2..0fab787589 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs @@ -1,4 +1,6 @@ -using Bit.Core.Models.Data; +#nullable enable + +using Bit.Core.Models.Data; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/Implementations/EventRouteService.cs b/src/Core/AdminConsole/Services/Implementations/EventRouteService.cs index a542e75a7b..df0819b409 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventRouteService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventRouteService.cs @@ -1,4 +1,6 @@ -using Bit.Core.Models.Data; +#nullable enable + +using Bit.Core.Models.Data; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/Implementations/EventService.cs b/src/Core/AdminConsole/Services/Implementations/EventService.cs index 0cecda61a7..88d9595b4a 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventService.cs @@ -462,13 +462,13 @@ public class EventService : IEventService private bool CanUseEvents(IDictionary orgAbilities, Guid orgId) { - return orgAbilities != null && orgAbilities.ContainsKey(orgId) && - orgAbilities[orgId].Enabled && orgAbilities[orgId].UseEvents; + return orgAbilities != null && orgAbilities.TryGetValue(orgId, out var orgAbility) && + orgAbility.Enabled && orgAbility.UseEvents; } private bool CanUseProviderEvents(IDictionary providerAbilities, Guid providerId) { - return providerAbilities != null && providerAbilities.ContainsKey(providerId) && - providerAbilities[providerId].Enabled && providerAbilities[providerId].UseEvents; + return providerAbilities != null && providerAbilities.TryGetValue(providerId, out var providerAbility) && + providerAbility.Enabled && providerAbility.UseEvents; } } diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 7640a82fcb..16e58d27ad 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -29,9 +29,6 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Settings; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Microsoft.Extensions.Logging; using Stripe; @@ -56,7 +53,6 @@ public class OrganizationService : IOrganizationService private readonly IPolicyRepository _policyRepository; private readonly IPolicyService _policyService; private readonly ISsoUserRepository _ssoUserRepository; - private readonly IReferenceEventService _referenceEventService; private readonly IGlobalSettings _globalSettings; private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; private readonly ICurrentContext _currentContext; @@ -88,7 +84,6 @@ public class OrganizationService : IOrganizationService IPolicyRepository policyRepository, IPolicyService policyService, ISsoUserRepository ssoUserRepository, - IReferenceEventService referenceEventService, IGlobalSettings globalSettings, IOrganizationApiKeyRepository organizationApiKeyRepository, ICurrentContext currentContext, @@ -120,7 +115,6 @@ public class OrganizationService : IOrganizationService _policyRepository = policyRepository; _policyService = policyService; _ssoUserRepository = ssoUserRepository; - _referenceEventService = referenceEventService; _globalSettings = globalSettings; _organizationApiKeyRepository = organizationApiKeyRepository; _currentContext = currentContext; @@ -153,11 +147,6 @@ public class OrganizationService : IOrganizationService } await _paymentService.CancelSubscriptionAsync(organization, eop); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.CancelSubscription, organization, _currentContext) - { - EndOfPeriod = endOfPeriod, - }); } public async Task ReinstateSubscriptionAsync(Guid organizationId) @@ -169,8 +158,6 @@ public class OrganizationService : IOrganizationService } await _paymentService.ReinstateSubscriptionAsync(organization); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.ReinstateSubscription, organization, _currentContext)); } public async Task AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb) @@ -190,13 +177,6 @@ public class OrganizationService : IOrganizationService var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, organization, storageAdjustmentGb, plan.PasswordManager.StripeStoragePlanId); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.AdjustStorage, organization, _currentContext) - { - PlanName = plan.Name, - PlanType = plan.Type, - Storage = storageAdjustmentGb, - }); await ReplaceAndUpdateCacheAsync(organization); return secret; } @@ -328,14 +308,6 @@ public class OrganizationService : IOrganizationService } var paymentIntentClientSecret = await _paymentService.AdjustSeatsAsync(organization, plan, additionalSeats); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.AdjustSeats, organization, _currentContext) - { - PlanName = plan.Name, - PlanType = plan.Type, - Seats = newSeatTotal, - PreviousSeats = organization.Seats - }); organization.Seats = (short?)newSeatTotal; await ReplaceAndUpdateCacheAsync(organization); @@ -640,12 +612,12 @@ public class OrganizationService : IOrganizationService } var providers = organization.GetTwoFactorProviders(); - if (!providers?.ContainsKey(type) ?? true) + if (providers is null || !providers.TryGetValue(type, out var provider)) { return; } - providers[type].Enabled = true; + provider.Enabled = true; organization.SetTwoFactorProviders(providers); await UpdateAsync(organization); } @@ -886,12 +858,6 @@ public class OrganizationService : IOrganizationService } await SendInvitesAsync(allOrgUsers, organization); - - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.InvitedUsers, organization, _currentContext) - { - Users = orgUserInvitedCount - }); } catch (Exception e) { @@ -1149,7 +1115,7 @@ public class OrganizationService : IOrganizationService var existingUsersDict = existingExternalUsers.ToDictionary(u => u.ExternalId); var removeUsersSet = new HashSet(removeUserExternalIds) .Except(newUsersSet) - .Where(u => existingUsersDict.ContainsKey(u) && existingUsersDict[u].Type != OrganizationUserType.Owner) + .Where(u => existingUsersDict.TryGetValue(u, out var existingUser) && existingUser.Type != OrganizationUserType.Owner) .Select(u => existingUsersDict[u]); await _organizationUserRepository.DeleteManyAsync(removeUsersSet.Select(u => u.Id)); @@ -1317,8 +1283,6 @@ public class OrganizationService : IOrganizationService } await _eventService.LogOrganizationUserEventsAsync(events.Select(e => (e.ou, e.e, eventSystemUser, e.d))); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.DirectorySynced, organization, _currentContext)); } public async Task DeleteSsoUserAsync(Guid userId, Guid? organizationId) @@ -1754,11 +1718,5 @@ public class OrganizationService : IOrganizationService await SendInviteAsync(ownerOrganizationUser, organization, true); await _eventService.LogOrganizationUserEventAsync(ownerOrganizationUser, EventType.OrganizationUser_Invited); - - await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.OrganizationCreatedByAdmin, organization, _currentContext) - { - EventRaisedByUser = userService.GetUserName(user), - SalesAssistedTrialStarted = salesAssistedTrialStarted, - }); } } diff --git a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs index c3eb2272d0..d424bd8fff 100644 --- a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs +++ b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs @@ -68,7 +68,7 @@ public class PolicyService : IPolicyService var excludedUserTypes = GetUserTypesExcludedFromPolicy(policyType); var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); return organizationUserPolicyDetails.Where(o => - (!orgAbilities.ContainsKey(o.OrganizationId) || orgAbilities[o.OrganizationId].UsePolicies) && + (!orgAbilities.TryGetValue(o.OrganizationId, out var orgAbility) || orgAbility.UsePolicies) && o.PolicyEnabled && !excludedUserTypes.Contains(o.OrganizationUserType) && o.OrganizationUserStatus >= minStatus && diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs index 74833f38a0..bc2329930d 100644 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs @@ -1,7 +1,6 @@ -using System.Text; -using System.Text.Json; -using Bit.Core.Models.Data; -using Bit.Core.Settings; +#nullable enable + +using System.Text; using Microsoft.Extensions.Logging; using RabbitMQ.Client; using RabbitMQ.Client.Events; @@ -10,94 +9,60 @@ namespace Bit.Core.Services; public class RabbitMqEventListenerService : EventLoggingListenerService { - private IChannel _channel; - private IConnection _connection; - private readonly string _exchangeName; - private readonly ConnectionFactory _factory; - private readonly ILogger _logger; + private readonly Lazy> _lazyChannel; private readonly string _queueName; + private readonly IRabbitMqService _rabbitMqService; public RabbitMqEventListenerService( IEventMessageHandler handler, - ILogger logger, - GlobalSettings globalSettings, - string queueName) : base(handler) + string queueName, + IRabbitMqService rabbitMqService, + ILogger logger) : base(handler, logger) { - _factory = new ConnectionFactory - { - HostName = globalSettings.EventLogging.RabbitMq.HostName, - UserName = globalSettings.EventLogging.RabbitMq.Username, - Password = globalSettings.EventLogging.RabbitMq.Password - }; - _exchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName; _logger = logger; _queueName = queueName; + _rabbitMqService = rabbitMqService; + _lazyChannel = new Lazy>(() => _rabbitMqService.CreateChannelAsync()); } public override async Task StartAsync(CancellationToken cancellationToken) { - _connection = await _factory.CreateConnectionAsync(cancellationToken); - _channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken); - - await _channel.ExchangeDeclareAsync(exchange: _exchangeName, - type: ExchangeType.Fanout, - durable: true, - cancellationToken: cancellationToken); - await _channel.QueueDeclareAsync(queue: _queueName, - durable: true, - exclusive: false, - autoDelete: false, - arguments: null, - cancellationToken: cancellationToken); - await _channel.QueueBindAsync(queue: _queueName, - exchange: _exchangeName, - routingKey: string.Empty, - cancellationToken: cancellationToken); + await _rabbitMqService.CreateEventQueueAsync(_queueName, cancellationToken); await base.StartAsync(cancellationToken); } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { - var consumer = new AsyncEventingBasicConsumer(_channel); - consumer.ReceivedAsync += async (_, eventArgs) => - { - try - { - using var jsonDocument = JsonDocument.Parse(Encoding.UTF8.GetString(eventArgs.Body.Span)); - var root = jsonDocument.RootElement; + var channel = await _lazyChannel.Value; + var consumer = new AsyncEventingBasicConsumer(channel); + consumer.ReceivedAsync += async (_, eventArgs) => { await ProcessReceivedMessageAsync(eventArgs); }; - if (root.ValueKind == JsonValueKind.Array) - { - var eventMessages = root.Deserialize>(); - await _handler.HandleManyEventsAsync(eventMessages); - } - else if (root.ValueKind == JsonValueKind.Object) - { - var eventMessage = root.Deserialize(); - await _handler.HandleEventAsync(eventMessage); + await channel.BasicConsumeAsync(_queueName, autoAck: true, consumer: consumer, cancellationToken: cancellationToken); + } - } - } - catch (Exception ex) - { - _logger.LogError(ex, "An error occurred while processing the message"); - } - }; - - await _channel.BasicConsumeAsync(_queueName, autoAck: true, consumer: consumer, cancellationToken: cancellationToken); + internal async Task ProcessReceivedMessageAsync(BasicDeliverEventArgs eventArgs) + { + await ProcessReceivedMessageAsync( + Encoding.UTF8.GetString(eventArgs.Body.Span), + eventArgs.BasicProperties.MessageId); } public override async Task StopAsync(CancellationToken cancellationToken) { - await _channel.CloseAsync(cancellationToken); - await _connection.CloseAsync(cancellationToken); + if (_lazyChannel.IsValueCreated) + { + var channel = await _lazyChannel.Value; + await channel.CloseAsync(cancellationToken); + } await base.StopAsync(cancellationToken); } public override void Dispose() { - _channel.Dispose(); - _connection.Dispose(); + if (_lazyChannel.IsValueCreated && _lazyChannel.Value.IsCompletedSuccessfully) + { + _lazyChannel.Value.Result.Dispose(); + } base.Dispose(); } } diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs deleted file mode 100644 index 05fcf71a92..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Text.Json; -using Bit.Core.Models.Data; -using Bit.Core.Settings; -using RabbitMQ.Client; - -namespace Bit.Core.Services; -public class RabbitMqEventWriteService : IEventWriteService, IAsyncDisposable -{ - private readonly ConnectionFactory _factory; - private readonly Lazy> _lazyConnection; - private readonly string _exchangeName; - - public RabbitMqEventWriteService(GlobalSettings globalSettings) - { - _factory = new ConnectionFactory - { - HostName = globalSettings.EventLogging.RabbitMq.HostName, - UserName = globalSettings.EventLogging.RabbitMq.Username, - Password = globalSettings.EventLogging.RabbitMq.Password - }; - _exchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName; - - _lazyConnection = new Lazy>(CreateConnectionAsync); - } - - public async Task CreateAsync(IEvent e) - { - var connection = await _lazyConnection.Value; - using var channel = await connection.CreateChannelAsync(); - - await channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true); - - var body = JsonSerializer.SerializeToUtf8Bytes(e); - - await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: string.Empty, body: body); - } - - public async Task CreateManyAsync(IEnumerable events) - { - var connection = await _lazyConnection.Value; - using var channel = await connection.CreateChannelAsync(); - await channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true); - - var body = JsonSerializer.SerializeToUtf8Bytes(events); - - await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: string.Empty, body: body); - } - - public async ValueTask DisposeAsync() - { - if (_lazyConnection.IsValueCreated) - { - var connection = await _lazyConnection.Value; - await connection.DisposeAsync(); - } - } - - private async Task CreateConnectionAsync() - { - return await _factory.CreateConnectionAsync(); - } -} diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs index 1d6910db95..5b18d8817c 100644 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs @@ -1,5 +1,8 @@ -using System.Text; -using Bit.Core.Settings; +#nullable enable + +using System.Text; +using System.Text.Json; +using Bit.Core.AdminConsole.Models.Data.Integrations; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using RabbitMQ.Client; @@ -9,183 +12,137 @@ namespace Bit.Core.Services; public class RabbitMqIntegrationListenerService : BackgroundService { - private const string _deadLetterRoutingKey = "dead-letter"; - private IChannel _channel; - private IConnection _connection; - private readonly string _exchangeName; - private readonly string _queueName; - private readonly string _retryQueueName; - private readonly string _deadLetterQueueName; - private readonly string _routingKey; - private readonly string _retryRoutingKey; private readonly int _maxRetries; + private readonly string _queueName; + private readonly string _routingKey; + private readonly string _retryQueueName; private readonly IIntegrationHandler _handler; - private readonly ConnectionFactory _factory; + private readonly Lazy> _lazyChannel; + private readonly IRabbitMqService _rabbitMqService; private readonly ILogger _logger; - private readonly int _retryTiming; public RabbitMqIntegrationListenerService(IIntegrationHandler handler, string routingKey, string queueName, string retryQueueName, - string deadLetterQueueName, - GlobalSettings globalSettings, + int maxRetries, + IRabbitMqService rabbitMqService, ILogger logger) { _handler = handler; _routingKey = routingKey; - _retryRoutingKey = $"{_routingKey}-retry"; - _queueName = queueName; _retryQueueName = retryQueueName; - _deadLetterQueueName = deadLetterQueueName; + _queueName = queueName; + _rabbitMqService = rabbitMqService; _logger = logger; - _exchangeName = globalSettings.EventLogging.RabbitMq.IntegrationExchangeName; - _maxRetries = globalSettings.EventLogging.RabbitMq.MaxRetries; - _retryTiming = globalSettings.EventLogging.RabbitMq.RetryTiming; - - _factory = new ConnectionFactory - { - HostName = globalSettings.EventLogging.RabbitMq.HostName, - UserName = globalSettings.EventLogging.RabbitMq.Username, - Password = globalSettings.EventLogging.RabbitMq.Password - }; + _maxRetries = maxRetries; + _lazyChannel = new Lazy>(() => _rabbitMqService.CreateChannelAsync()); } public override async Task StartAsync(CancellationToken cancellationToken) { - _connection = await _factory.CreateConnectionAsync(cancellationToken); - _channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken); - - await _channel.ExchangeDeclareAsync(exchange: _exchangeName, - type: ExchangeType.Direct, - durable: true, - cancellationToken: cancellationToken); - - // Declare main queue - await _channel.QueueDeclareAsync(queue: _queueName, - durable: true, - exclusive: false, - autoDelete: false, - arguments: null, - cancellationToken: cancellationToken); - await _channel.QueueBindAsync(queue: _queueName, - exchange: _exchangeName, - routingKey: _routingKey, - cancellationToken: cancellationToken); - - // Declare retry queue (Configurable TTL, dead-letters back to main queue) - await _channel.QueueDeclareAsync(queue: _retryQueueName, - durable: true, - exclusive: false, - autoDelete: false, - arguments: new Dictionary - { - { "x-dead-letter-exchange", _exchangeName }, - { "x-dead-letter-routing-key", _routingKey }, - { "x-message-ttl", _retryTiming } - }, - cancellationToken: cancellationToken); - await _channel.QueueBindAsync(queue: _retryQueueName, - exchange: _exchangeName, - routingKey: _retryRoutingKey, - cancellationToken: cancellationToken); - - // Declare dead letter queue - await _channel.QueueDeclareAsync(queue: _deadLetterQueueName, - durable: true, - exclusive: false, - autoDelete: false, - arguments: null, - cancellationToken: cancellationToken); - await _channel.QueueBindAsync(queue: _deadLetterQueueName, - exchange: _exchangeName, - routingKey: _deadLetterRoutingKey, - cancellationToken: cancellationToken); + await _rabbitMqService.CreateIntegrationQueuesAsync( + _queueName, + _retryQueueName, + _routingKey, + cancellationToken: cancellationToken); await base.StartAsync(cancellationToken); } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { - var consumer = new AsyncEventingBasicConsumer(_channel); + var channel = await _lazyChannel.Value; + var consumer = new AsyncEventingBasicConsumer(channel); consumer.ReceivedAsync += async (_, ea) => + { + await ProcessReceivedMessageAsync(ea, cancellationToken); + }; + + await channel.BasicConsumeAsync(queue: _queueName, autoAck: false, consumer: consumer, cancellationToken: cancellationToken); + } + + internal async Task ProcessReceivedMessageAsync(BasicDeliverEventArgs ea, CancellationToken cancellationToken) + { + var channel = await _lazyChannel.Value; + try { var json = Encoding.UTF8.GetString(ea.Body.Span); - try + // Determine if the message came off of the retry queue too soon + // If so, place it back on the retry queue + var integrationMessage = JsonSerializer.Deserialize(json); + if (integrationMessage is not null && + integrationMessage.DelayUntilDate.HasValue && + integrationMessage.DelayUntilDate.Value > DateTime.UtcNow) { - var result = await _handler.HandleAsync(json); - var message = result.Message; + await _rabbitMqService.RepublishToRetryQueueAsync(channel, ea); + await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); + return; + } - if (result.Success) + var result = await _handler.HandleAsync(json); + var message = result.Message; + + if (result.Success) + { + // Successful integration send. Acknowledge message delivery and return + await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); + return; + } + + if (result.Retryable) + { + // Integration failed, but is retryable - apply delay and check max retries + message.ApplyRetry(result.DelayUntilDate); + + if (message.RetryCount < _maxRetries) { - // Successful integration send. Acknowledge message delivery and return - await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); - return; - } - - if (result.Retryable) - { - // Integration failed, but is retryable - apply delay and check max retries - message.ApplyRetry(result.DelayUntilDate); - - if (message.RetryCount < _maxRetries) - { - // Publish message to the retry queue. It will be re-published for retry after a delay - await _channel.BasicPublishAsync( - exchange: _exchangeName, - routingKey: _retryRoutingKey, - body: Encoding.UTF8.GetBytes(message.ToJson()), - cancellationToken: cancellationToken); - } - else - { - // Exceeded the max number of retries; fail and send to dead letter queue - await PublishToDeadLetterAsync(message.ToJson()); - _logger.LogWarning("Max retry attempts reached. Sent to DLQ."); - } + // Publish message to the retry queue. It will be re-published for retry after a delay + await _rabbitMqService.PublishToRetryAsync(channel, message, cancellationToken); } else { - // Fatal error (i.e. not retryable) occurred. Send message to dead letter queue without any retries - await PublishToDeadLetterAsync(message.ToJson()); - _logger.LogWarning("Non-retryable failure. Sent to DLQ."); + // Exceeded the max number of retries; fail and send to dead letter queue + await _rabbitMqService.PublishToDeadLetterAsync(channel, message, cancellationToken); + _logger.LogWarning("Max retry attempts reached. Sent to DLQ."); } - - // Message has been sent to retry or dead letter queues. - // Acknowledge receipt so Rabbit knows it's been processed - await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); } - catch (Exception ex) + else { - // Unknown error occurred. Acknowledge so Rabbit doesn't keep attempting. Log the error - _logger.LogError(ex, "Unhandled error processing integration message."); - await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); + // Fatal error (i.e. not retryable) occurred. Send message to dead letter queue without any retries + await _rabbitMqService.PublishToDeadLetterAsync(channel, message, cancellationToken); + _logger.LogWarning("Non-retryable failure. Sent to DLQ."); } - }; - await _channel.BasicConsumeAsync(queue: _queueName, autoAck: false, consumer: consumer, cancellationToken: cancellationToken); - } - - private async Task PublishToDeadLetterAsync(string json) - { - await _channel.BasicPublishAsync( - exchange: _exchangeName, - routingKey: _deadLetterRoutingKey, - body: Encoding.UTF8.GetBytes(json)); + // Message has been sent to retry or dead letter queues. + // Acknowledge receipt so Rabbit knows it's been processed + await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); + } + catch (Exception ex) + { + // Unknown error occurred. Acknowledge so Rabbit doesn't keep attempting. Log the error + _logger.LogError(ex, "Unhandled error processing integration message."); + await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); + } } public override async Task StopAsync(CancellationToken cancellationToken) { - await _channel.CloseAsync(cancellationToken); - await _connection.CloseAsync(cancellationToken); + if (_lazyChannel.IsValueCreated) + { + var channel = await _lazyChannel.Value; + await channel.CloseAsync(cancellationToken); + } await base.StopAsync(cancellationToken); } public override void Dispose() { - _channel.Dispose(); - _connection.Dispose(); + if (_lazyChannel.IsValueCreated && _lazyChannel.Value.IsCompletedSuccessfully) + { + _lazyChannel.Value.Result.Dispose(); + } base.Dispose(); } } diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationPublisher.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationPublisher.cs deleted file mode 100644 index 12801e3216..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationPublisher.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Text; -using Bit.Core.AdminConsole.Models.Data.Integrations; -using Bit.Core.Enums; -using Bit.Core.Settings; -using RabbitMQ.Client; - -namespace Bit.Core.Services; - -public class RabbitMqIntegrationPublisher : IIntegrationPublisher, IAsyncDisposable -{ - private readonly ConnectionFactory _factory; - private readonly Lazy> _lazyConnection; - private readonly string _exchangeName; - - public RabbitMqIntegrationPublisher(GlobalSettings globalSettings) - { - _factory = new ConnectionFactory - { - HostName = globalSettings.EventLogging.RabbitMq.HostName, - UserName = globalSettings.EventLogging.RabbitMq.Username, - Password = globalSettings.EventLogging.RabbitMq.Password - }; - _exchangeName = globalSettings.EventLogging.RabbitMq.IntegrationExchangeName; - - _lazyConnection = new Lazy>(CreateConnectionAsync); - } - - public async Task PublishAsync(IIntegrationMessage message) - { - var routingKey = message.IntegrationType.ToRoutingKey(); - var connection = await _lazyConnection.Value; - await using var channel = await connection.CreateChannelAsync(); - - await channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Direct, durable: true); - - var body = Encoding.UTF8.GetBytes(message.ToJson()); - - await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: routingKey, body: body); - } - - public async ValueTask DisposeAsync() - { - if (_lazyConnection.IsValueCreated) - { - var connection = await _lazyConnection.Value; - await connection.DisposeAsync(); - } - } - - private async Task CreateConnectionAsync() - { - return await _factory.CreateConnectionAsync(); - } -} diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqService.cs new file mode 100644 index 0000000000..617d1b41fb --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqService.cs @@ -0,0 +1,244 @@ +#nullable enable + +using System.Text; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Enums; +using Bit.Core.Settings; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace Bit.Core.Services; + +public class RabbitMqService : IRabbitMqService +{ + private const string _deadLetterRoutingKey = "dead-letter"; + + private readonly ConnectionFactory _factory; + private readonly Lazy> _lazyConnection; + private readonly string _deadLetterQueueName; + private readonly string _eventExchangeName; + private readonly string _integrationExchangeName; + private readonly int _retryTiming; + private readonly bool _useDelayPlugin; + + public RabbitMqService(GlobalSettings globalSettings) + { + _factory = new ConnectionFactory + { + HostName = globalSettings.EventLogging.RabbitMq.HostName, + UserName = globalSettings.EventLogging.RabbitMq.Username, + Password = globalSettings.EventLogging.RabbitMq.Password + }; + _deadLetterQueueName = globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName; + _eventExchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName; + _integrationExchangeName = globalSettings.EventLogging.RabbitMq.IntegrationExchangeName; + _retryTiming = globalSettings.EventLogging.RabbitMq.RetryTiming; + _useDelayPlugin = globalSettings.EventLogging.RabbitMq.UseDelayPlugin; + + _lazyConnection = new Lazy>(CreateConnectionAsync); + } + + public async Task CreateChannelAsync(CancellationToken cancellationToken = default) + { + var connection = await _lazyConnection.Value; + return await connection.CreateChannelAsync(cancellationToken: cancellationToken); + } + + public async Task CreateEventQueueAsync(string queueName, CancellationToken cancellationToken = default) + { + using var channel = await CreateChannelAsync(cancellationToken); + await channel.QueueDeclareAsync(queue: queueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null, + cancellationToken: cancellationToken); + await channel.QueueBindAsync(queue: queueName, + exchange: _eventExchangeName, + routingKey: string.Empty, + cancellationToken: cancellationToken); + } + + public async Task CreateIntegrationQueuesAsync( + string queueName, + string retryQueueName, + string routingKey, + CancellationToken cancellationToken = default) + { + using var channel = await CreateChannelAsync(cancellationToken); + var retryRoutingKey = $"{routingKey}-retry"; + + // Declare main integration queue + await channel.QueueDeclareAsync( + queue: queueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null, + cancellationToken: cancellationToken); + await channel.QueueBindAsync( + queue: queueName, + exchange: _integrationExchangeName, + routingKey: routingKey, + cancellationToken: cancellationToken); + + if (!_useDelayPlugin) + { + // Declare retry queue (Configurable TTL, dead-letters back to main queue) + // Only needed if NOT using delay plugin + await channel.QueueDeclareAsync(queue: retryQueueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: new Dictionary + { + { "x-dead-letter-exchange", _integrationExchangeName }, + { "x-dead-letter-routing-key", routingKey }, + { "x-message-ttl", _retryTiming } + }, + cancellationToken: cancellationToken); + await channel.QueueBindAsync(queue: retryQueueName, + exchange: _integrationExchangeName, + routingKey: retryRoutingKey, + cancellationToken: cancellationToken); + } + } + + public async Task PublishAsync(IIntegrationMessage message) + { + var routingKey = message.IntegrationType.ToRoutingKey(); + await using var channel = await CreateChannelAsync(); + + var body = Encoding.UTF8.GetBytes(message.ToJson()); + var properties = new BasicProperties + { + MessageId = message.MessageId, + Persistent = true + }; + + await channel.BasicPublishAsync( + exchange: _integrationExchangeName, + mandatory: true, + basicProperties: properties, + routingKey: routingKey, + body: body); + } + + public async Task PublishEventAsync(string body) + { + await using var channel = await CreateChannelAsync(); + var properties = new BasicProperties + { + MessageId = Guid.NewGuid().ToString(), + Persistent = true + }; + + await channel.BasicPublishAsync( + exchange: _eventExchangeName, + mandatory: true, + basicProperties: properties, + routingKey: string.Empty, + body: Encoding.UTF8.GetBytes(body)); + } + + public async Task PublishToRetryAsync(IChannel channel, IIntegrationMessage message, CancellationToken cancellationToken) + { + var routingKey = message.IntegrationType.ToRoutingKey(); + var retryRoutingKey = $"{routingKey}-retry"; + var properties = new BasicProperties + { + Persistent = true, + MessageId = message.MessageId, + Headers = _useDelayPlugin && message.DelayUntilDate.HasValue ? + new Dictionary + { + ["x-delay"] = Math.Max((int)(message.DelayUntilDate.Value - DateTime.UtcNow).TotalMilliseconds, 0) + } : + null + }; + + await channel.BasicPublishAsync( + exchange: _integrationExchangeName, + routingKey: _useDelayPlugin ? routingKey : retryRoutingKey, + mandatory: true, + basicProperties: properties, + body: Encoding.UTF8.GetBytes(message.ToJson()), + cancellationToken: cancellationToken); + } + + public async Task PublishToDeadLetterAsync( + IChannel channel, + IIntegrationMessage message, + CancellationToken cancellationToken) + { + var properties = new BasicProperties + { + MessageId = message.MessageId, + Persistent = true + }; + + await channel.BasicPublishAsync( + exchange: _integrationExchangeName, + mandatory: true, + basicProperties: properties, + routingKey: _deadLetterRoutingKey, + body: Encoding.UTF8.GetBytes(message.ToJson()), + cancellationToken: cancellationToken); + } + + public async Task RepublishToRetryQueueAsync(IChannel channel, BasicDeliverEventArgs eventArgs) + { + await channel.BasicPublishAsync( + exchange: _integrationExchangeName, + routingKey: eventArgs.RoutingKey, + mandatory: true, + basicProperties: new BasicProperties(eventArgs.BasicProperties), + body: eventArgs.Body); + } + + public async ValueTask DisposeAsync() + { + if (_lazyConnection.IsValueCreated) + { + var connection = await _lazyConnection.Value; + await connection.DisposeAsync(); + } + } + + private async Task CreateConnectionAsync() + { + var connection = await _factory.CreateConnectionAsync(); + using var channel = await connection.CreateChannelAsync(); + + // Declare Exchanges + await channel.ExchangeDeclareAsync(exchange: _eventExchangeName, type: ExchangeType.Fanout, durable: true); + if (_useDelayPlugin) + { + await channel.ExchangeDeclareAsync( + exchange: _integrationExchangeName, + type: "x-delayed-message", + durable: true, + arguments: new Dictionary + { + { "x-delayed-type", "direct" } + } + ); + } + else + { + await channel.ExchangeDeclareAsync(exchange: _integrationExchangeName, type: ExchangeType.Direct, durable: true); + } + + // Declare dead letter queue for Integration exchange + await channel.QueueDeclareAsync(queue: _deadLetterQueueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null); + await channel.QueueBindAsync(queue: _deadLetterQueueName, + exchange: _integrationExchangeName, + routingKey: _deadLetterRoutingKey); + + return connection; + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs index 134e93e838..fe0f6eabe1 100644 --- a/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs @@ -1,4 +1,6 @@ -using Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +using Bit.Core.AdminConsole.Models.Data.Integrations; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/Implementations/SlackService.cs b/src/Core/AdminConsole/Services/Implementations/SlackService.cs index effcfdf1ce..3f82217830 100644 --- a/src/Core/AdminConsole/Services/Implementations/SlackService.cs +++ b/src/Core/AdminConsole/Services/Implementations/SlackService.cs @@ -1,4 +1,6 @@ -using System.Net.Http.Headers; +#nullable enable + +using System.Net.Http.Headers; using System.Net.Http.Json; using System.Web; using Bit.Core.Models.Slack; @@ -22,7 +24,7 @@ public class SlackService( public async Task GetChannelIdAsync(string token, string channelName) { - return (await GetChannelIdsAsync(token, [channelName])).FirstOrDefault(); + return (await GetChannelIdsAsync(token, [channelName])).FirstOrDefault() ?? string.Empty; } public async Task> GetChannelIdsAsync(string token, List channelNames) @@ -58,7 +60,7 @@ public class SlackService( } else { - logger.LogError("Error getting Channel Ids: {Error}", result.Error); + logger.LogError("Error getting Channel Ids: {Error}", result?.Error ?? "Unknown Error"); nextCursor = string.Empty; } @@ -89,7 +91,7 @@ public class SlackService( new KeyValuePair("redirect_uri", redirectUrl) })); - SlackOAuthResponse result; + SlackOAuthResponse? result; try { result = await tokenResponse.Content.ReadFromJsonAsync(); @@ -99,7 +101,7 @@ public class SlackService( result = null; } - if (result == null) + if (result is null) { logger.LogError("Error obtaining token via OAuth: Unknown error"); return string.Empty; @@ -130,6 +132,11 @@ public class SlackService( var response = await _httpClient.SendAsync(request); var result = await response.Content.ReadFromJsonAsync(); + if (result is null) + { + logger.LogError("Error retrieving Slack user ID: Unknown error"); + return string.Empty; + } if (!result.Ok) { logger.LogError("Error retrieving Slack user ID: {Error}", result.Error); @@ -151,6 +158,11 @@ public class SlackService( var response = await _httpClient.SendAsync(request); var result = await response.Content.ReadFromJsonAsync(); + if (result is null) + { + logger.LogError("Error opening DM channel: Unknown error"); + return string.Empty; + } if (!result.Ok) { logger.LogError("Error opening DM channel: {Error}", result.Error); diff --git a/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs index 5f9898afe8..df364b2a48 100644 --- a/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs @@ -1,4 +1,6 @@ -using System.Globalization; +#nullable enable + +using System.Globalization; using System.Net; using System.Text; using Bit.Core.AdminConsole.Models.Data.Integrations; @@ -29,7 +31,7 @@ public class WebhookIntegrationHandler(IHttpClientFactory httpClientFactory) case HttpStatusCode.ServiceUnavailable: case HttpStatusCode.GatewayTimeout: result.Retryable = true; - result.FailureReason = response.ReasonPhrase; + result.FailureReason = response.ReasonPhrase ?? $"Failure with status code: {(int)response.StatusCode}"; if (response.Headers.TryGetValues("Retry-After", out var values)) { @@ -52,7 +54,7 @@ public class WebhookIntegrationHandler(IHttpClientFactory httpClientFactory) break; default: result.Retryable = false; - result.FailureReason = response.ReasonPhrase; + result.FailureReason = response.ReasonPhrase ?? $"Failure with status code {(int)response.StatusCode}"; break; } diff --git a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs index 718e44ae5f..2f8481cea2 100644 --- a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs @@ -43,7 +43,7 @@ public class EmailTwoFactorTokenProvider : EmailTokenProvider private static bool HasProperMetaData(TwoFactorProvider provider) { - return provider?.MetaData != null && provider.MetaData.ContainsKey("Email") && - !string.IsNullOrWhiteSpace((string)provider.MetaData["Email"]); + return provider?.MetaData != null && provider.MetaData.TryGetValue("Email", out var emailValue) && + !string.IsNullOrWhiteSpace((string)emailValue); } } diff --git a/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs index 0bf75d0fc3..3b4b0fa520 100644 --- a/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs @@ -80,7 +80,7 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); var keys = LoadKeys(provider); - if (!provider.MetaData.TryGetValue("login", out var value)) + if (!provider.MetaData.TryGetValue("login", out var login)) { return false; } @@ -88,7 +88,7 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider var clientResponse = JsonSerializer.Deserialize(token, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - var jsonOptions = value.ToString(); + var jsonOptions = login.ToString(); var options = AssertionOptions.FromJson(jsonOptions); var webAuthCred = keys.Find(k => k.Item2.Descriptor.Id.SequenceEqual(clientResponse.Id)); @@ -148,9 +148,9 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider for (var i = 1; i <= 5; i++) { var keyName = $"Key{i}"; - if (provider.MetaData.ContainsKey(keyName)) + if (provider.MetaData.TryGetValue(keyName, out var value)) { - var key = new TwoFactorProvider.WebAuthnData((dynamic)provider.MetaData[keyName]); + var key = new TwoFactorProvider.WebAuthnData((dynamic)value); keys.Add(new Tuple(keyName, key)); } diff --git a/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs b/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs index e721649dc9..289bbff7f8 100644 --- a/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs +++ b/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs @@ -3,7 +3,6 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; @@ -11,9 +10,6 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tokens; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Identity; @@ -26,15 +22,12 @@ public class RegisterUserCommand : IRegisterUserCommand private readonly IGlobalSettings _globalSettings; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IPolicyRepository _policyRepository; - private readonly IReferenceEventService _referenceEventService; private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; private readonly IDataProtectorTokenFactory _registrationEmailVerificationTokenDataFactory; private readonly IDataProtector _organizationServiceDataProtector; private readonly IDataProtector _providerServiceDataProtector; - private readonly ICurrentContext _currentContext; - private readonly IUserService _userService; private readonly IMailService _mailService; @@ -48,11 +41,9 @@ public class RegisterUserCommand : IRegisterUserCommand IGlobalSettings globalSettings, IOrganizationUserRepository organizationUserRepository, IPolicyRepository policyRepository, - IReferenceEventService referenceEventService, IDataProtectionProvider dataProtectionProvider, IDataProtectorTokenFactory orgUserInviteTokenDataFactory, IDataProtectorTokenFactory registrationEmailVerificationTokenDataFactory, - ICurrentContext currentContext, IUserService userService, IMailService mailService, IValidateRedemptionTokenCommand validateRedemptionTokenCommand, @@ -62,14 +53,12 @@ public class RegisterUserCommand : IRegisterUserCommand _globalSettings = globalSettings; _organizationUserRepository = organizationUserRepository; _policyRepository = policyRepository; - _referenceEventService = referenceEventService; _organizationServiceDataProtector = dataProtectionProvider.CreateProtector( "OrganizationServiceDataProtector"); _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; _registrationEmailVerificationTokenDataFactory = registrationEmailVerificationTokenDataFactory; - _currentContext = currentContext; _userService = userService; _mailService = mailService; @@ -86,7 +75,6 @@ public class RegisterUserCommand : IRegisterUserCommand if (result == IdentityResult.Success) { await _mailService.SendWelcomeEmailAsync(user); - await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext)); } return result; @@ -119,12 +107,6 @@ public class RegisterUserCommand : IRegisterUserCommand sentWelcomeEmail = true; if (!string.IsNullOrEmpty(initiationPath)) { - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext) - { - SignupInitiationPath = initiationPath - }); - return result; } } @@ -134,8 +116,6 @@ public class RegisterUserCommand : IRegisterUserCommand { await _mailService.SendWelcomeEmailAsync(user); } - - await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext)); } return result; @@ -263,10 +243,6 @@ public class RegisterUserCommand : IRegisterUserCommand if (result == IdentityResult.Success) { await _mailService.SendWelcomeEmailAsync(user); - await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext) - { - ReceiveMarketingEmails = tokenable.ReceiveMarketingEmails - }); } return result; @@ -285,7 +261,6 @@ public class RegisterUserCommand : IRegisterUserCommand if (result == IdentityResult.Success) { await _mailService.SendWelcomeEmailAsync(user); - await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext)); } return result; @@ -306,7 +281,6 @@ public class RegisterUserCommand : IRegisterUserCommand if (result == IdentityResult.Success) { await _mailService.SendWelcomeEmailAsync(user); - await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext)); } return result; @@ -325,7 +299,6 @@ public class RegisterUserCommand : IRegisterUserCommand if (result == IdentityResult.Success) { await _mailService.SendWelcomeEmailAsync(user); - await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext)); } return result; diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 3769cafc5c..e6a822452a 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -23,6 +23,7 @@ public static class Constants public const string Fido2KeyCipherMinimumVersion = "2023.10.0"; public const string SSHKeyCipherMinimumVersion = "2024.12.0"; + public const string DenyLegacyUserMinimumVersion = "2025.6.0"; /// /// Used by IdentityServer to identify our own provider. @@ -144,7 +145,6 @@ public static class FeatureFlagKeys public const string PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships"; public const string UsePricingService = "use-pricing-service"; public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features"; - public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method"; public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates"; 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"; diff --git a/src/Core/Context/CurrentContext.cs b/src/Core/Context/CurrentContext.cs index cbd90055b0..68d4606907 100644 --- a/src/Core/Context/CurrentContext.cs +++ b/src/Core/Context/CurrentContext.cs @@ -64,39 +64,39 @@ public class CurrentContext : ICurrentContext HttpContext = httpContext; await BuildAsync(httpContext.User, globalSettings); - if (DeviceIdentifier == null && httpContext.Request.Headers.ContainsKey("Device-Identifier")) + if (DeviceIdentifier == null && httpContext.Request.Headers.TryGetValue("Device-Identifier", out var deviceIdentifier)) { - DeviceIdentifier = httpContext.Request.Headers["Device-Identifier"]; + DeviceIdentifier = deviceIdentifier; } - if (httpContext.Request.Headers.ContainsKey("Device-Type") && - Enum.TryParse(httpContext.Request.Headers["Device-Type"].ToString(), out DeviceType dType)) + if (httpContext.Request.Headers.TryGetValue("Device-Type", out var deviceType) && + Enum.TryParse(deviceType.ToString(), out DeviceType dType)) { DeviceType = dType; } - if (!BotScore.HasValue && httpContext.Request.Headers.ContainsKey("X-Cf-Bot-Score") && - int.TryParse(httpContext.Request.Headers["X-Cf-Bot-Score"], out var parsedBotScore)) + if (!BotScore.HasValue && httpContext.Request.Headers.TryGetValue("X-Cf-Bot-Score", out var cfBotScore) && + int.TryParse(cfBotScore, out var parsedBotScore)) { BotScore = parsedBotScore; } - if (httpContext.Request.Headers.ContainsKey("X-Cf-Worked-Proxied")) + if (httpContext.Request.Headers.TryGetValue("X-Cf-Worked-Proxied", out var cfWorkedProxied)) { - CloudflareWorkerProxied = httpContext.Request.Headers["X-Cf-Worked-Proxied"] == "1"; + CloudflareWorkerProxied = cfWorkedProxied == "1"; } - if (httpContext.Request.Headers.ContainsKey("X-Cf-Is-Bot")) + if (httpContext.Request.Headers.TryGetValue("X-Cf-Is-Bot", out var cfIsBot)) { - IsBot = httpContext.Request.Headers["X-Cf-Is-Bot"] == "1"; + IsBot = cfIsBot == "1"; } - if (httpContext.Request.Headers.ContainsKey("X-Cf-Maybe-Bot")) + if (httpContext.Request.Headers.TryGetValue("X-Cf-Maybe-Bot", out var cfMaybeBot)) { - MaybeBot = httpContext.Request.Headers["X-Cf-Maybe-Bot"] == "1"; + MaybeBot = cfMaybeBot == "1"; } - if (httpContext.Request.Headers.ContainsKey("Bitwarden-Client-Version") && Version.TryParse(httpContext.Request.Headers["Bitwarden-Client-Version"], out var cVersion)) + if (httpContext.Request.Headers.TryGetValue("Bitwarden-Client-Version", out var bitWardenClientVersion) && Version.TryParse(bitWardenClientVersion, out var cVersion)) { ClientVersion = cVersion; } @@ -190,14 +190,14 @@ public class CurrentContext : ICurrentContext private List GetOrganizations(Dictionary> claimsDict, bool orgApi) { - var accessSecretsManager = claimsDict.ContainsKey(Claims.SecretsManagerAccess) - ? claimsDict[Claims.SecretsManagerAccess].ToDictionary(s => s.Value, _ => true) + var accessSecretsManager = claimsDict.TryGetValue(Claims.SecretsManagerAccess, out var secretsManagerAccessClaim) + ? secretsManagerAccessClaim.ToDictionary(s => s.Value, _ => true) : new Dictionary(); var organizations = new List(); - if (claimsDict.ContainsKey(Claims.OrganizationOwner)) + if (claimsDict.TryGetValue(Claims.OrganizationOwner, out var organizationOwnerClaim)) { - organizations.AddRange(claimsDict[Claims.OrganizationOwner].Select(c => + organizations.AddRange(organizationOwnerClaim.Select(c => new CurrentContextOrganization { Id = new Guid(c.Value), @@ -214,9 +214,9 @@ public class CurrentContext : ICurrentContext }); } - if (claimsDict.ContainsKey(Claims.OrganizationAdmin)) + if (claimsDict.TryGetValue(Claims.OrganizationAdmin, out var organizationAdminClaim)) { - organizations.AddRange(claimsDict[Claims.OrganizationAdmin].Select(c => + organizations.AddRange(organizationAdminClaim.Select(c => new CurrentContextOrganization { Id = new Guid(c.Value), @@ -225,9 +225,9 @@ public class CurrentContext : ICurrentContext })); } - if (claimsDict.ContainsKey(Claims.OrganizationUser)) + if (claimsDict.TryGetValue(Claims.OrganizationUser, out var organizationUserClaim)) { - organizations.AddRange(claimsDict[Claims.OrganizationUser].Select(c => + organizations.AddRange(organizationUserClaim.Select(c => new CurrentContextOrganization { Id = new Guid(c.Value), @@ -236,9 +236,9 @@ public class CurrentContext : ICurrentContext })); } - if (claimsDict.ContainsKey(Claims.OrganizationCustom)) + if (claimsDict.TryGetValue(Claims.OrganizationCustom, out var organizationCustomClaim)) { - organizations.AddRange(claimsDict[Claims.OrganizationCustom].Select(c => + organizations.AddRange(organizationCustomClaim.Select(c => new CurrentContextOrganization { Id = new Guid(c.Value), @@ -254,9 +254,9 @@ public class CurrentContext : ICurrentContext private List GetProviders(Dictionary> claimsDict) { var providers = new List(); - if (claimsDict.ContainsKey(Claims.ProviderAdmin)) + if (claimsDict.TryGetValue(Claims.ProviderAdmin, out var providerAdminClaim)) { - providers.AddRange(claimsDict[Claims.ProviderAdmin].Select(c => + providers.AddRange(providerAdminClaim.Select(c => new CurrentContextProvider { Id = new Guid(c.Value), @@ -264,9 +264,9 @@ public class CurrentContext : ICurrentContext })); } - if (claimsDict.ContainsKey(Claims.ProviderServiceUser)) + if (claimsDict.TryGetValue(Claims.ProviderServiceUser, out var providerServiceUserClaim)) { - providers.AddRange(claimsDict[Claims.ProviderServiceUser].Select(c => + providers.AddRange(providerServiceUserClaim.Select(c => new CurrentContextProvider { Id = new Guid(c.Value), @@ -504,20 +504,20 @@ public class CurrentContext : ICurrentContext private string GetClaimValue(Dictionary> claims, string type) { - if (!claims.ContainsKey(type)) + if (!claims.TryGetValue(type, out var claim)) { return null; } - return claims[type].FirstOrDefault()?.Value; + return claim.FirstOrDefault()?.Value; } private Permissions SetOrganizationPermissionsFromClaims(string organizationId, Dictionary> claimsDict) { bool hasClaim(string claimKey) { - return claimsDict.ContainsKey(claimKey) ? - claimsDict[claimKey].Any(x => x.Value == organizationId) : false; + return claimsDict.TryGetValue(claimKey, out var claim) ? + claim.Any(x => x.Value == organizationId) : false; } return new Permissions diff --git a/src/Core/Entities/User.cs b/src/Core/Entities/User.cs index 08981ca2d3..b92d22b0e3 100644 --- a/src/Core/Entities/User.cs +++ b/src/Core/Entities/User.cs @@ -3,7 +3,6 @@ using System.Text.Json; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Enums; -using Bit.Core.Tools.Entities; using Bit.Core.Utilities; using Microsoft.AspNetCore.Identity; @@ -11,7 +10,7 @@ using Microsoft.AspNetCore.Identity; namespace Bit.Core.Entities; -public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFactorProvidersUser, IReferenceable +public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFactorProvidersUser { private Dictionary? _twoFactorProviders; @@ -196,12 +195,7 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider) { var providers = GetTwoFactorProviders(); - if (providers == null || !providers.TryGetValue(provider, out var value)) - { - return null; - } - - return value; + return providers?.GetValueOrDefault(provider); } public long StorageBytesRemaining() diff --git a/src/Core/IdentityServer/DistributedCacheCookieManager.cs b/src/Core/IdentityServer/DistributedCacheCookieManager.cs index 9771b40662..5d6717ac41 100644 --- a/src/Core/IdentityServer/DistributedCacheCookieManager.cs +++ b/src/Core/IdentityServer/DistributedCacheCookieManager.cs @@ -63,6 +63,6 @@ public class DistributedCacheCookieManager : ICookieManager private string GetKey(string key, string id) => $"{CacheKeyPrefix}-{key}-{id}"; private string GetId(HttpContext context, string key) => - context.Request.Cookies.ContainsKey(key) ? - context.Request.Cookies[key] : null; + context.Request.Cookies.TryGetValue(key, out var cookie) ? + cookie : null; } diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs index cb37e478f7..838c1e97b9 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs @@ -8,7 +8,6 @@ using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; -using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; @@ -16,9 +15,6 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions; @@ -30,9 +26,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand private readonly IPaymentService _paymentService; private readonly IPolicyRepository _policyRepository; private readonly ISsoConfigRepository _ssoConfigRepository; - private readonly IReferenceEventService _referenceEventService; private readonly IOrganizationConnectionRepository _organizationConnectionRepository; - private readonly ICurrentContext _currentContext; private readonly IServiceAccountRepository _serviceAccountRepository; private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationService _organizationService; @@ -47,9 +41,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand IPaymentService paymentService, IPolicyRepository policyRepository, ISsoConfigRepository ssoConfigRepository, - IReferenceEventService referenceEventService, IOrganizationConnectionRepository organizationConnectionRepository, - ICurrentContext currentContext, IServiceAccountRepository serviceAccountRepository, IOrganizationRepository organizationRepository, IOrganizationService organizationService, @@ -63,9 +55,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand _paymentService = paymentService; _policyRepository = policyRepository; _ssoConfigRepository = ssoConfigRepository; - _referenceEventService = referenceEventService; _organizationConnectionRepository = organizationConnectionRepository; - _currentContext = currentContext; _serviceAccountRepository = serviceAccountRepository; _organizationRepository = organizationRepository; _organizationService = organizationService; @@ -285,25 +275,6 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand } await _organizationService.ReplaceAndUpdateCacheAsync(organization); - - if (success) - { - var upgradePath = GetUpgradePath(existingPlan.ProductTier, newPlan.ProductTier); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.UpgradePlan, organization, _currentContext) - { - PlanName = newPlan.Name, - PlanType = newPlan.Type, - OldPlanName = existingPlan.Name, - OldPlanType = existingPlan.Type, - Seats = organization.Seats, - SignupInitiationPath = "Upgrade in-product", - PlanUpgradePath = upgradePath, - Storage = organization.MaxStorageGb, - // TODO: add reference events for SmSeats and Service Accounts - see AC-1481 - }); - } - return new Tuple(success, paymentIntentClientSecret); } diff --git a/src/Core/Services/Implementations/CollectionService.cs b/src/Core/Services/Implementations/CollectionService.cs index 46853447d6..852f2da073 100644 --- a/src/Core/Services/Implementations/CollectionService.cs +++ b/src/Core/Services/Implementations/CollectionService.cs @@ -1,13 +1,9 @@ #nullable enable -using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Repositories; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; namespace Bit.Core.Services; @@ -17,23 +13,17 @@ public class CollectionService : ICollectionService private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly ICollectionRepository _collectionRepository; - private readonly IReferenceEventService _referenceEventService; - private readonly ICurrentContext _currentContext; public CollectionService( IEventService eventService, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, - ICollectionRepository collectionRepository, - IReferenceEventService referenceEventService, - ICurrentContext currentContext) + ICollectionRepository collectionRepository) { _eventService = eventService; _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; _collectionRepository = collectionRepository; - _referenceEventService = referenceEventService; - _currentContext = currentContext; } public async Task SaveAsync(Collection collection, IEnumerable? groups = null, @@ -78,7 +68,6 @@ public class CollectionService : ICollectionService await _collectionRepository.CreateAsync(collection, org.UseGroups ? groupsList : null, usersList); await _eventService.LogCollectionEventAsync(collection, Enums.EventType.Collection_Created); - await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.CollectionCreated, org, _currentContext)); } else { diff --git a/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs b/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs index 436e354954..0fde6d8906 100644 --- a/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs +++ b/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs @@ -50,14 +50,7 @@ public class InMemoryApplicationCacheService : IApplicationCacheService await InitProviderAbilitiesAsync(); var newAbility = new ProviderAbility(provider); - if (_providerAbilities.ContainsKey(provider.Id)) - { - _providerAbilities[provider.Id] = newAbility; - } - else - { - _providerAbilities.Add(provider.Id, newAbility); - } + _providerAbilities[provider.Id] = newAbility; } public virtual async Task UpsertOrganizationAbilityAsync(Organization organization) @@ -65,32 +58,19 @@ public class InMemoryApplicationCacheService : IApplicationCacheService await InitOrganizationAbilitiesAsync(); var newAbility = new OrganizationAbility(organization); - if (_orgAbilities.ContainsKey(organization.Id)) - { - _orgAbilities[organization.Id] = newAbility; - } - else - { - _orgAbilities.Add(organization.Id, newAbility); - } + _orgAbilities[organization.Id] = newAbility; } public virtual Task DeleteOrganizationAbilityAsync(Guid organizationId) { - if (_orgAbilities != null && _orgAbilities.ContainsKey(organizationId)) - { - _orgAbilities.Remove(organizationId); - } + _orgAbilities?.Remove(organizationId); return Task.FromResult(0); } public virtual Task DeleteProviderAbilityAsync(Guid providerId) { - if (_providerAbilities != null && _providerAbilities.ContainsKey(providerId)) - { - _providerAbilities.Remove(providerId); - } + _providerAbilities?.Remove(providerId); return Task.FromResult(0); } diff --git a/src/Core/Services/Implementations/LicensingService.cs b/src/Core/Services/Implementations/LicensingService.cs index dd603b4b63..2d91017ce2 100644 --- a/src/Core/Services/Implementations/LicensingService.cs +++ b/src/Core/Services/Implementations/LicensingService.cs @@ -182,9 +182,8 @@ public class LicensingService : ILicensingService // Only check once per day var now = DateTime.UtcNow; - if (_userCheckCache.ContainsKey(user.Id)) + if (_userCheckCache.TryGetValue(user.Id, out var lastCheck)) { - var lastCheck = _userCheckCache[user.Id]; if (lastCheck < now && now - lastCheck < TimeSpan.FromDays(1)) { return user.Premium; diff --git a/src/Core/Services/Implementations/SendGridMailDeliveryService.cs b/src/Core/Services/Implementations/SendGridMailDeliveryService.cs index a35d119970..ea915b56f2 100644 --- a/src/Core/Services/Implementations/SendGridMailDeliveryService.cs +++ b/src/Core/Services/Implementations/SendGridMailDeliveryService.cs @@ -72,8 +72,8 @@ public class SendGridMailDeliveryService : IMailDeliveryService, IDisposable msg.SetOpenTracking(false); if (message.MetaData != null && - message.MetaData.ContainsKey("SendGridBypassListManagement") && - Convert.ToBoolean(message.MetaData["SendGridBypassListManagement"])) + message.MetaData.TryGetValue("SendGridBypassListManagement", out var sendGridBypassListManagement) && + Convert.ToBoolean(sendGridBypassListManagement)) { msg.SetBypassListManagement(true); } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index f0c97b8589..bf90190ee6 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -30,9 +30,6 @@ using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Settings; using Bit.Core.Tokens; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Bit.Core.Vault.Repositories; using Fido2NetLib; @@ -69,7 +66,6 @@ public class UserService : UserManager, IUserService, IDisposable private readonly IPolicyRepository _policyRepository; private readonly IPolicyService _policyService; private readonly IDataProtector _organizationServiceDataProtector; - private readonly IReferenceEventService _referenceEventService; private readonly IFido2 _fido2; private readonly ICurrentContext _currentContext; private readonly IGlobalSettings _globalSettings; @@ -109,7 +105,6 @@ public class UserService : UserManager, IUserService, IDisposable IPaymentService paymentService, IPolicyRepository policyRepository, IPolicyService policyService, - IReferenceEventService referenceEventService, IFido2 fido2, ICurrentContext currentContext, IGlobalSettings globalSettings, @@ -154,7 +149,6 @@ public class UserService : UserManager, IUserService, IDisposable _policyService = policyService; _organizationServiceDataProtector = dataProtectionProvider.CreateProtector( "OrganizationServiceDataProtector"); - _referenceEventService = referenceEventService; _fido2 = fido2; _currentContext = currentContext; _globalSettings = globalSettings; @@ -299,8 +293,6 @@ public class UserService : UserManager, IUserService, IDisposable } await _userRepository.DeleteAsync(user); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.DeleteAccount, user, _currentContext)); await _pushService.PushLogOutAsync(user.Id); return IdentityResult.Success; } @@ -365,12 +357,12 @@ public class UserService : UserManager, IUserService, IDisposable public async Task SendTwoFactorEmailAsync(User user, bool authentication = true) { var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); - if (provider == null || provider.MetaData == null || !provider.MetaData.ContainsKey("Email")) + if (provider == null || provider.MetaData == null || !provider.MetaData.TryGetValue("Email", out var emailValue)) { throw new ArgumentNullException("No email."); } - var email = ((string)provider.MetaData["Email"]).ToLowerInvariant(); + var email = ((string)emailValue).ToLowerInvariant(); var token = await base.GenerateTwoFactorTokenAsync(user, CoreHelpers.CustomProviderName(TwoFactorProviderType.Email)); @@ -398,12 +390,12 @@ public class UserService : UserManager, IUserService, IDisposable public async Task VerifyTwoFactorEmailAsync(User user, string token) { var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); - if (provider == null || provider.MetaData == null || !provider.MetaData.ContainsKey("Email")) + if (provider == null || provider.MetaData == null || !provider.MetaData.TryGetValue("Email", out var emailValue)) { throw new ArgumentNullException("No email."); } - var email = ((string)provider.MetaData["Email"]).ToLowerInvariant(); + var email = ((string)emailValue).ToLowerInvariant(); return await base.VerifyTwoFactorTokenAsync(user, CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), token); } @@ -461,12 +453,12 @@ public class UserService : UserManager, IUserService, IDisposable var keyId = $"Key{id}"; var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); - if (!provider?.MetaData?.ContainsKey("pending") ?? true) + if (provider?.MetaData is null || !provider.MetaData.TryGetValue("pending", out var pendingValue)) { return false; } - var options = CredentialCreateOptions.FromJson((string)provider.MetaData["pending"]); + var options = CredentialCreateOptions.FromJson((string)pendingValue); // Callback to ensure credential ID is unique. Always return true since we don't care if another // account uses the same 2FA key. @@ -1046,12 +1038,6 @@ public class UserService : UserManager, IUserService, IDisposable { await SaveUserAsync(user); await _pushService.PushSyncVaultAsync(user.Id); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.UpgradePlan, user, _currentContext) - { - Storage = user.MaxStorageGb, - PlanName = PremiumPlanId, - }); } catch when (!_globalSettings.SelfHosted) { @@ -1117,12 +1103,6 @@ public class UserService : UserManager, IUserService, IDisposable var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb, StripeConstants.Prices.StoragePlanPersonal); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.AdjustStorage, user, _currentContext) - { - Storage = storageAdjustmentGb, - PlanName = StripeConstants.Prices.StoragePlanPersonal, - }); await SaveUserAsync(user); return secret; } @@ -1150,18 +1130,11 @@ public class UserService : UserManager, IUserService, IDisposable eop = false; } await _paymentService.CancelSubscriptionAsync(user, eop); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.CancelSubscription, user, _currentContext) - { - EndOfPeriod = eop - }); } public async Task ReinstatePremiumAsync(User user) { await _paymentService.ReinstateSubscriptionAsync(user); - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.ReinstateSubscription, user, _currentContext)); } public async Task EnablePremiumAsync(Guid userId, DateTime? expirationDate) @@ -1380,14 +1353,14 @@ public class UserService : UserManager, IUserService, IDisposable public void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool setEnabled = true) { var providers = user.GetTwoFactorProviders(); - if (!providers?.ContainsKey(type) ?? true) + if (providers is null || !providers.TryGetValue(type, out var provider)) { return; } if (setEnabled) { - providers[type].Enabled = true; + provider.Enabled = true; } user.SetTwoFactorProviders(providers); @@ -1446,17 +1419,6 @@ public class UserService : UserManager, IUserService, IDisposable await Task.WhenAll(legacyRevokeOrgUserTasks); } - public override async Task ConfirmEmailAsync(User user, string token) - { - var result = await base.ConfirmEmailAsync(user, token); - if (result.Succeeded) - { - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.ConfirmEmailAddress, user, _currentContext)); - } - return result; - } - public async Task RotateApiKeyAsync(User user) { user.ApiKey = CoreHelpers.SecureRandomString(30); diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index b821f214db..f08d66c28f 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -327,6 +327,7 @@ public class GlobalSettings : IGlobalSettings public int MaxRetries { get; set; } = 3; public int RetryTiming { get; set; } = 30000; // 30s + public bool UseDelayPlugin { get; set; } = false; public virtual string EventRepositoryQueueName { get; set; } = "events-write-queue"; public virtual string IntegrationDeadLetterQueueName { get; set; } = "integration-dead-letter-queue"; public virtual string SlackEventsQueueName { get; set; } = "events-slack-queue"; diff --git a/src/Core/Tools/Entities/IReferenceable.cs b/src/Core/Tools/Entities/IReferenceable.cs deleted file mode 100644 index 4b38ec6ccc..0000000000 --- a/src/Core/Tools/Entities/IReferenceable.cs +++ /dev/null @@ -1,29 +0,0 @@ -#nullable enable -using Bit.Core.Tools.Models.Business; - -namespace Bit.Core.Tools.Entities; - -/// -/// An entity that can be referenced by a . -/// -public interface IReferenceable -{ - /// - /// Identifies the entity that generated the event. - /// - Guid Id { get; set; } - - /// - /// Contextual information included in the event. - /// - /// - /// Do not store secrets in this field. - /// - string? ReferenceData { get; set; } - - /// - /// Returns when the entity is a user. - /// Otherwise returns . - /// - bool IsUser(); -} diff --git a/src/Core/Tools/Enums/ReferenceEventSource.cs b/src/Core/Tools/Enums/ReferenceEventSource.cs deleted file mode 100644 index 87a71cf450..0000000000 --- a/src/Core/Tools/Enums/ReferenceEventSource.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Runtime.Serialization; - -namespace Bit.Core.Tools.Enums; - -public enum ReferenceEventSource -{ - [EnumMember(Value = "organization")] - Organization, - [EnumMember(Value = "user")] - User, - [EnumMember(Value = "provider")] - Provider, - [EnumMember(Value = "registration")] - Registration, -} diff --git a/src/Core/Tools/Enums/ReferenceEventType.cs b/src/Core/Tools/Enums/ReferenceEventType.cs deleted file mode 100644 index a1446b9fc4..0000000000 --- a/src/Core/Tools/Enums/ReferenceEventType.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Runtime.Serialization; - -namespace Bit.Core.Tools.Enums; - -public enum ReferenceEventType -{ - [EnumMember(Value = "signup-email-submit")] - SignupEmailSubmit, - [EnumMember(Value = "signup-email-clicked")] - SignupEmailClicked, - [EnumMember(Value = "signup")] - Signup, - [EnumMember(Value = "upgrade-plan")] - UpgradePlan, - [EnumMember(Value = "adjust-storage")] - AdjustStorage, - [EnumMember(Value = "adjust-seats")] - AdjustSeats, - [EnumMember(Value = "cancel-subscription")] - CancelSubscription, - [EnumMember(Value = "reinstate-subscription")] - ReinstateSubscription, - [EnumMember(Value = "delete-account")] - DeleteAccount, - [EnumMember(Value = "confirm-email")] - ConfirmEmailAddress, - [EnumMember(Value = "invited-users")] - InvitedUsers, - [EnumMember(Value = "rebilled")] - Rebilled, - [EnumMember(Value = "send-created")] - SendCreated, - [EnumMember(Value = "send-accessed")] - SendAccessed, - [EnumMember(Value = "directory-synced")] - DirectorySynced, - [EnumMember(Value = "vault-imported")] - VaultImported, - [EnumMember(Value = "cipher-created")] - CipherCreated, - [EnumMember(Value = "group-created")] - GroupCreated, - [EnumMember(Value = "collection-created")] - CollectionCreated, - [EnumMember(Value = "organization-edited-by-admin")] - OrganizationEditedByAdmin, - [EnumMember(Value = "organization-created-by-admin")] - OrganizationCreatedByAdmin, - [EnumMember(Value = "organization-edited-in-stripe")] - OrganizationEditedInStripe, - [EnumMember(Value = "sm-service-account-accessed-secret")] - SmServiceAccountAccessedSecret, -} diff --git a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs index fd7a82172c..f67a2550d2 100644 --- a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs +++ b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs @@ -2,16 +2,12 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; -using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Tools.Enums; using Bit.Core.Tools.ImportFeatures.Interfaces; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; @@ -27,8 +23,6 @@ public class ImportCiphersCommand : IImportCiphersCommand private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly ICollectionRepository _collectionRepository; - private readonly IReferenceEventService _referenceEventService; - private readonly ICurrentContext _currentContext; private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly IFeatureService _featureService; @@ -40,8 +34,6 @@ public class ImportCiphersCommand : IImportCiphersCommand IOrganizationUserRepository organizationUserRepository, IPushNotificationService pushService, IPolicyService policyService, - IReferenceEventService referenceEventService, - ICurrentContext currentContext, IPolicyRequirementQuery policyRequirementQuery, IFeatureService featureService) { @@ -52,8 +44,6 @@ public class ImportCiphersCommand : IImportCiphersCommand _collectionRepository = collectionRepository; _pushService = pushService; _policyService = policyService; - _referenceEventService = referenceEventService; - _currentContext = currentContext; _policyRequirementQuery = policyRequirementQuery; _featureService = featureService; } @@ -194,12 +184,5 @@ public class ImportCiphersCommand : IImportCiphersCommand // push await _pushService.PushSyncVaultAsync(importingUserId); - - - if (org != null) - { - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.VaultImported, org, _currentContext)); - } } } diff --git a/src/Core/Tools/Models/Business/ReferenceEvent.cs b/src/Core/Tools/Models/Business/ReferenceEvent.cs deleted file mode 100644 index a93817ca44..0000000000 --- a/src/Core/Tools/Models/Business/ReferenceEvent.cs +++ /dev/null @@ -1,274 +0,0 @@ -#nullable enable - -using System.Text.Json.Serialization; -using Bit.Core.Billing.Enums; -using Bit.Core.Context; -using Bit.Core.Tools.Entities; -using Bit.Core.Tools.Enums; - -namespace Bit.Core.Tools.Models.Business; - -/// -/// Product support monitoring. -/// -/// -/// Do not store secrets in this type. -/// -public class ReferenceEvent -{ - /// - /// Instantiates a . - /// - public ReferenceEvent() { } - - /// - /// Monitored event type. - /// Entity that created the event. - /// The conditions in which the event occurred. - public ReferenceEvent(ReferenceEventType type, IReferenceable source, ICurrentContext currentContext) - { - Type = type; - if (source != null) - { - Source = source.IsUser() ? ReferenceEventSource.User : ReferenceEventSource.Organization; - Id = source.Id; - ReferenceData = source.ReferenceData; - } - if (currentContext != null) - { - ClientId = currentContext.ClientId; - ClientVersion = currentContext.ClientVersion; - } - } - - /// - /// Monitored event type. - /// - [JsonConverter(typeof(JsonStringEnumConverter))] - public ReferenceEventType Type { get; set; } - - /// - /// The kind of entity that created the event. - /// - [JsonConverter(typeof(JsonStringEnumConverter))] - public ReferenceEventSource Source { get; set; } - - /// - public Guid Id { get; set; } - - /// - public string? ReferenceData { get; set; } - - /// - /// Moment the event occurred. - /// - public DateTime EventDate { get; set; } = DateTime.UtcNow; - - /// - /// Number of users sent invitations by an organization. - /// - /// - /// Should contain a value only on events. - /// Otherwise the value should be . - /// - public int? Users { get; set; } - - /// - /// Whether or not a subscription was canceled immediately or at the end of the billing period. - /// - /// - /// when a cancellation occurs immediately. - /// when a cancellation occurs at the end of a customer's billing period. - /// Should contain a value only on events. - /// Otherwise the value should be . - /// - public bool? EndOfPeriod { get; set; } - - /// - /// Branded name of the subscription. - /// - /// - /// Should contain a value only for subscription management events. - /// Otherwise the value should be . - /// - public string? PlanName { get; set; } - - /// - /// Identifies a subscription. - /// - /// - /// Should contain a value only for subscription management events. - /// Otherwise the value should be . - /// - public PlanType? PlanType { get; set; } - - /// - /// The branded name of the prior plan. - /// - /// - /// Should contain a value only on events - /// initiated by organizations. - /// Otherwise the value should be . - /// - public string? OldPlanName { get; set; } - - /// - /// Identifies the prior plan - /// - /// - /// Should contain a value only on events - /// initiated by organizations. - /// Otherwise the value should be . - /// - public PlanType? OldPlanType { get; set; } - - /// - /// Seat count when a billable action occurs. When adjusting seats, contains - /// the new seat count. - /// - /// - /// Should contain a value only on , - /// , , - /// and events initiated by organizations. - /// Otherwise the value should be . - /// - public int? Seats { get; set; } - - /// - /// Seat count when a seat adjustment occurs. - /// - /// - /// Should contain a value only on - /// events initiated by organizations. - /// Otherwise the value should be . - /// - public int? PreviousSeats { get; set; } - - /// - /// Qty in GB of storage. When adjusting storage, contains the adjusted - /// storage qty. Otherwise contains the total storage quantity. - /// - /// - /// Should contain a value only on , - /// , , - /// and events. - /// Otherwise the value should be . - /// - public short? Storage { get; set; } - - /// - /// The type of send created or accessed. - /// - /// - /// Should contain a value only on - /// and events. - /// Otherwise the value should be . - /// - [JsonConverter(typeof(JsonStringEnumConverter))] - public SendType? SendType { get; set; } - - /// - /// Whether the send has private notes. - /// - /// - /// when the send has private notes, otherwise . - /// Should contain a value only on - /// and events. - /// Otherwise the value should be . - /// - public bool? SendHasNotes { get; set; } - - /// - /// The send expires after its access count exceeds this value. - /// - /// - /// This field only contains a value when the send has a max access count - /// and is - /// or events. - /// Otherwise, the value should be . - /// - public int? MaxAccessCount { get; set; } - - /// - /// Whether the created send has a password. - /// - /// - /// Should contain a value only on - /// and events. - /// Otherwise the value should be . - /// - public bool? HasPassword { get; set; } - - /// - /// The administrator that performed the action. - /// - /// - /// Should contain a value only on - /// and events. - /// Otherwise the value should be . - /// - public string? EventRaisedByUser { get; set; } - - /// - /// Whether or not an organization's trial period was started by a sales person. - /// - /// - /// Should contain a value only on - /// and events. - /// Otherwise the value should be . - /// - public bool? SalesAssistedTrialStarted { get; set; } - - /// - /// The installation id of the application that originated the event. - /// - /// - /// when the event was not originated by an application. - /// - public string? ClientId { get; set; } - - /// - /// The version of the client application that originated the event. - /// - /// - /// when the event was not originated by an application. - /// - public Version? ClientVersion { get; set; } - - /// - /// The initiation path of a user who signed up for a paid version of Bitwarden. For example, "Trial from marketing website". - /// - /// - /// This value should only be populated when the is . Otherwise, - /// the value should be . - /// - public string? SignupInitiationPath { get; set; } - - /// - /// The upgrade applied to an account. The current plan is listed first, - /// followed by the plan they are migrating to. For example, - /// "Teams Starter → Teams, Enterprise". - /// - /// - /// when the event was not originated by an application, - /// or when a downgrade occurred. - /// - public string? PlanUpgradePath { get; set; } - - /// - /// Used for the event to determine if the user has opted in to marketing emails. - /// - public bool? ReceiveMarketingEmails { get; set; } - - /// - /// Used for the event to indicate if the user - /// landed on the registration finish screen with a valid or invalid email verification token. - /// - public bool? EmailVerificationTokenValid { get; set; } - - /// - /// Used for the event to indicate if the user - /// landed on the registration finish screen after re-clicking an already used link. - /// - public bool? UserAlreadyExists { get; set; } -} diff --git a/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs b/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs index 00da0a911f..87b4e581ca 100644 --- a/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs +++ b/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs @@ -1,10 +1,8 @@ using System.Text.Json; -using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Platform.Push; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Repositories; using Bit.Core.Tools.SendFeatures.Commands.Interfaces; @@ -19,8 +17,6 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand private readonly ISendFileStorageService _sendFileStorageService; private readonly IPushNotificationService _pushNotificationService; private readonly ISendValidationService _sendValidationService; - private readonly IReferenceEventService _referenceEventService; - private readonly ICurrentContext _currentContext; private readonly ISendCoreHelperService _sendCoreHelperService; public NonAnonymousSendCommand(ISendRepository sendRepository, @@ -28,16 +24,12 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand IPushNotificationService pushNotificationService, ISendAuthorizationService sendAuthorizationService, ISendValidationService sendValidationService, - IReferenceEventService referenceEventService, - ICurrentContext currentContext, ISendCoreHelperService sendCoreHelperService) { _sendRepository = sendRepository; _sendFileStorageService = sendFileStorageService; _pushNotificationService = pushNotificationService; _sendValidationService = sendValidationService; - _referenceEventService = referenceEventService; - _currentContext = currentContext; _sendCoreHelperService = sendCoreHelperService; } @@ -50,18 +42,6 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand { await _sendRepository.CreateAsync(send); await _pushNotificationService.PushSyncSendCreateAsync(send); - await _referenceEventService.RaiseEventAsync(new ReferenceEvent - { - Id = send.UserId ?? default, - Type = ReferenceEventType.SendCreated, - Source = ReferenceEventSource.User, - SendType = send.Type, - MaxAccessCount = send.MaxAccessCount, - HasPassword = !string.IsNullOrWhiteSpace(send.Password), - SendHasNotes = send.Data?.Contains("Notes"), - ClientId = _currentContext.ClientId, - ClientVersion = _currentContext.ClientVersion - }); } else { diff --git a/src/Core/Tools/SendFeatures/Services/SendAuthorizationService.cs b/src/Core/Tools/SendFeatures/Services/SendAuthorizationService.cs index 101a33754e..cf9f2636c0 100644 --- a/src/Core/Tools/SendFeatures/Services/SendAuthorizationService.cs +++ b/src/Core/Tools/SendFeatures/Services/SendAuthorizationService.cs @@ -1,9 +1,7 @@ -using Bit.Core.Context; -using Bit.Core.Entities; +using Bit.Core.Entities; using Bit.Core.Platform.Push; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Repositories; using Microsoft.AspNetCore.Identity; @@ -15,21 +13,15 @@ public class SendAuthorizationService : ISendAuthorizationService private readonly ISendRepository _sendRepository; private readonly IPasswordHasher _passwordHasher; private readonly IPushNotificationService _pushNotificationService; - private readonly IReferenceEventService _referenceEventService; - private readonly ICurrentContext _currentContext; public SendAuthorizationService( ISendRepository sendRepository, IPasswordHasher passwordHasher, - IPushNotificationService pushNotificationService, - IReferenceEventService referenceEventService, - ICurrentContext currentContext) + IPushNotificationService pushNotificationService) { _sendRepository = sendRepository; _passwordHasher = passwordHasher; _pushNotificationService = pushNotificationService; - _referenceEventService = referenceEventService; - _currentContext = currentContext; } public SendAccessResult SendCanBeAccessed(Send send, @@ -79,18 +71,6 @@ public class SendAuthorizationService : ISendAuthorizationService await _sendRepository.ReplaceAsync(sendToBeAccessed); await _pushNotificationService.PushSyncSendUpdateAsync(sendToBeAccessed); - await _referenceEventService.RaiseEventAsync(new ReferenceEvent - { - Id = sendToBeAccessed.UserId ?? default, - Type = ReferenceEventType.SendAccessed, - Source = ReferenceEventSource.User, - SendType = sendToBeAccessed.Type, - MaxAccessCount = sendToBeAccessed.MaxAccessCount, - HasPassword = !string.IsNullOrWhiteSpace(sendToBeAccessed.Password), - SendHasNotes = sendToBeAccessed.Data?.Contains("Notes"), - ClientId = _currentContext.ClientId, - ClientVersion = _currentContext.ClientVersion - }); return accessResult; } diff --git a/src/Core/Tools/Services/IReferenceEventService.cs b/src/Core/Tools/Services/IReferenceEventService.cs deleted file mode 100644 index d8fe12b5c7..0000000000 --- a/src/Core/Tools/Services/IReferenceEventService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Bit.Core.Tools.Models.Business; - -namespace Bit.Core.Tools.Services; - -public interface IReferenceEventService -{ - Task RaiseEventAsync(ReferenceEvent referenceEvent); -} diff --git a/src/Core/Tools/Services/Implementations/AzureQueueReferenceEventService.cs b/src/Core/Tools/Services/Implementations/AzureQueueReferenceEventService.cs deleted file mode 100644 index 52769bdafb..0000000000 --- a/src/Core/Tools/Services/Implementations/AzureQueueReferenceEventService.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Text; -using System.Text.Json; -using Azure.Storage.Queues; -using Bit.Core.Settings; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Utilities; - -namespace Bit.Core.Tools.Services; - -public class AzureQueueReferenceEventService : IReferenceEventService -{ - private const string _queueName = "reference-events"; - - private readonly QueueClient _queueClient; - private readonly GlobalSettings _globalSettings; - - public AzureQueueReferenceEventService( - GlobalSettings globalSettings) - { - _queueClient = new QueueClient(globalSettings.Events.ConnectionString, _queueName); - _globalSettings = globalSettings; - } - - public async Task RaiseEventAsync(ReferenceEvent referenceEvent) - { - await SendMessageAsync(referenceEvent); - } - - private async Task SendMessageAsync(ReferenceEvent referenceEvent) - { - if (_globalSettings.SelfHosted) - { - // Ignore for self-hosted - return; - } - try - { - var message = JsonSerializer.Serialize(referenceEvent, JsonHelpers.IgnoreWritingNullAndCamelCase); - // Messages need to be base64 encoded - var encodedMessage = Convert.ToBase64String(Encoding.UTF8.GetBytes(message)); - await _queueClient.SendMessageAsync(encodedMessage); - } - catch - { - // Ignore failure - } - } -} diff --git a/src/Core/Tools/Services/NoopImplementations/NoopReferenceEventService.cs b/src/Core/Tools/Services/NoopImplementations/NoopReferenceEventService.cs deleted file mode 100644 index aecdf4a80d..0000000000 --- a/src/Core/Tools/Services/NoopImplementations/NoopReferenceEventService.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Bit.Core.Tools.Models.Business; - -namespace Bit.Core.Tools.Services; - -public class NoopReferenceEventService : IReferenceEventService -{ - public Task RaiseEventAsync(ReferenceEvent referenceEvent) - { - return Task.CompletedTask; - } -} diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index eaf1f9fbba..14a2ec35e5 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -637,9 +637,9 @@ public static class CoreHelpers return null; } - if (!globalSettings.SelfHosted && httpContext.Request.Headers.ContainsKey(RealConnectingIp)) + if (!globalSettings.SelfHosted && httpContext.Request.Headers.TryGetValue(RealConnectingIp, out var realConnectingIp)) { - return httpContext.Request.Headers[RealConnectingIp].ToString(); + return realConnectingIp.ToString(); } return httpContext.Connection?.RemoteIpAddress?.ToString(); diff --git a/src/Core/Utilities/LoggerFactoryExtensions.cs b/src/Core/Utilities/LoggerFactoryExtensions.cs index 362ca22d34..b2388bc499 100644 --- a/src/Core/Utilities/LoggerFactoryExtensions.cs +++ b/src/Core/Utilities/LoggerFactoryExtensions.cs @@ -46,7 +46,7 @@ public static class LoggerFactoryExtensions { return true; } - var eventId = e.Properties.ContainsKey("EventId") ? e.Properties["EventId"].ToString() : null; + var eventId = e.Properties.TryGetValue("EventId", out var eventIdValue) ? eventIdValue.ToString() : null; if (eventId?.Contains(Constants.BypassFiltersEventId.ToString()) ?? false) { return true; diff --git a/src/Core/Vault/Services/ICipherService.cs b/src/Core/Vault/Services/ICipherService.cs index 7eeb6d2463..d3f8d20c90 100644 --- a/src/Core/Vault/Services/ICipherService.cs +++ b/src/Core/Vault/Services/ICipherService.cs @@ -24,7 +24,7 @@ public interface ICipherService Task DeleteFolderAsync(Folder folder); Task ShareAsync(Cipher originalCipher, Cipher cipher, Guid organizationId, IEnumerable collectionIds, Guid userId, DateTime? lastKnownRevisionDate); - Task ShareManyAsync(IEnumerable<(Cipher cipher, DateTime? lastKnownRevisionDate)> ciphers, Guid organizationId, + Task> ShareManyAsync(IEnumerable<(CipherDetails cipher, DateTime? lastKnownRevisionDate)> ciphers, Guid organizationId, IEnumerable collectionIds, Guid sharingUserId); Task SaveCollectionsAsync(Cipher cipher, IEnumerable collectionIds, Guid savingUserId, bool orgAdmin); Task SoftDeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false); diff --git a/src/Core/Vault/Services/Implementations/AzureAttachmentStorageService.cs b/src/Core/Vault/Services/Implementations/AzureAttachmentStorageService.cs index 20e4816662..89b152a645 100644 --- a/src/Core/Vault/Services/Implementations/AzureAttachmentStorageService.cs +++ b/src/Core/Vault/Services/Implementations/AzureAttachmentStorageService.cs @@ -251,16 +251,17 @@ public class AzureAttachmentStorageService : IAttachmentStorageService private async Task InitAsync(string containerName) { - if (!_attachmentContainers.ContainsKey(containerName) || _attachmentContainers[containerName] == null) + if (!_attachmentContainers.TryGetValue(containerName, out var attachmentContainer) || attachmentContainer == null) { - _attachmentContainers[containerName] = _blobServiceClient.GetBlobContainerClient(containerName); + attachmentContainer = _blobServiceClient.GetBlobContainerClient(containerName); + _attachmentContainers[containerName] = attachmentContainer; if (containerName == "attachments") { - await _attachmentContainers[containerName].CreateIfNotExistsAsync(PublicAccessType.Blob, null, null); + await attachmentContainer.CreateIfNotExistsAsync(PublicAccessType.Blob, null, null); } else { - await _attachmentContainers[containerName].CreateIfNotExistsAsync(PublicAccessType.None, null, null); + await attachmentContainer.CreateIfNotExistsAsync(PublicAccessType.None, null, null); } } } diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index f81e404db8..413aee3e0d 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -3,16 +3,12 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; -using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Bit.Core.Vault.Authorization.Permissions; using Bit.Core.Vault.Entities; @@ -41,8 +37,6 @@ public class CipherService : ICipherService private readonly IPolicyService _policyService; private readonly GlobalSettings _globalSettings; private const long _fileSizeLeeway = 1024L * 1024L; // 1MB - private readonly IReferenceEventService _referenceEventService; - private readonly ICurrentContext _currentContext; private readonly IGetCipherPermissionsForUserQuery _getCipherPermissionsForUserQuery; private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly IApplicationCacheService _applicationCacheService; @@ -62,8 +56,6 @@ public class CipherService : ICipherService IUserService userService, IPolicyService policyService, GlobalSettings globalSettings, - IReferenceEventService referenceEventService, - ICurrentContext currentContext, IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery, IPolicyRequirementQuery policyRequirementQuery, IApplicationCacheService applicationCacheService, @@ -82,8 +74,6 @@ public class CipherService : ICipherService _userService = userService; _policyService = policyService; _globalSettings = globalSettings; - _referenceEventService = referenceEventService; - _currentContext = currentContext; _getCipherPermissionsForUserQuery = getCipherPermissionsForUserQuery; _policyRequirementQuery = policyRequirementQuery; _applicationCacheService = applicationCacheService; @@ -108,9 +98,6 @@ public class CipherService : ICipherService cipher.UserId = savingUserId; } await _cipherRepository.CreateAsync(cipher, collectionIds); - - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.CipherCreated, await _organizationRepository.GetByIdAsync(cipher.OrganizationId.Value), _currentContext)); } else { @@ -325,13 +312,11 @@ public class CipherService : ICipherService } var attachments = cipher.GetAttachments(); - if (!attachments.ContainsKey(attachmentId)) + if (!attachments.TryGetValue(attachmentId, out var originalAttachmentMetadata)) { throw new BadRequestException($"Cipher does not own specified attachment"); } - var originalAttachmentMetadata = attachments[attachmentId]; - if (originalAttachmentMetadata.TempMetadata != null) { throw new BadRequestException("Another process is trying to migrate this attachment"); @@ -408,12 +393,11 @@ public class CipherService : ICipherService { var attachments = cipher?.GetAttachments() ?? new Dictionary(); - if (!attachments.ContainsKey(attachmentId)) + if (!attachments.TryGetValue(attachmentId, out var data)) { throw new NotFoundException(); } - var data = attachments[attachmentId]; var response = new AttachmentResponseData { Cipher = cipher, @@ -641,7 +625,7 @@ public class CipherService : ICipherService await _pushService.PushSyncCipherUpdateAsync(cipher, collectionIds); } - public async Task ShareManyAsync(IEnumerable<(Cipher cipher, DateTime? lastKnownRevisionDate)> cipherInfos, + public async Task> ShareManyAsync(IEnumerable<(CipherDetails cipher, DateTime? lastKnownRevisionDate)> cipherInfos, Guid organizationId, IEnumerable collectionIds, Guid sharingUserId) { var cipherIds = new List(); @@ -668,6 +652,7 @@ public class CipherService : ICipherService // push await _pushService.PushSyncCiphersAsync(sharingUserId); + return cipherInfos.Select(c => c.cipher); } public async Task SaveCollectionsAsync(Cipher cipher, IEnumerable collectionIds, Guid savingUserId, diff --git a/src/Events/Controllers/CollectController.cs b/src/Events/Controllers/CollectController.cs index 5e0417586f..5eb48a2688 100644 --- a/src/Events/Controllers/CollectController.cs +++ b/src/Events/Controllers/CollectController.cs @@ -69,9 +69,9 @@ public class CollectController : Controller continue; } Cipher cipher = null; - if (ciphersCache.ContainsKey(eventModel.CipherId.Value)) + if (ciphersCache.TryGetValue(eventModel.CipherId.Value, out var cachedCipher)) { - cipher = ciphersCache[eventModel.CipherId.Value]; + cipher = cachedCipher; } else { @@ -96,10 +96,7 @@ public class CollectController : Controller continue; } } - if (!ciphersCache.ContainsKey(eventModel.CipherId.Value)) - { - ciphersCache.Add(eventModel.CipherId.Value, cipher); - } + ciphersCache.TryAdd(eventModel.CipherId.Value, cipher); cipherEvents.Add(new Tuple(cipher, eventModel.Type, eventModel.Date)); break; case EventType.Organization_ClientExportedVault: diff --git a/src/Events/Program.cs b/src/Events/Program.cs index 6b53e4b18c..967e94ed83 100644 --- a/src/Events/Program.cs +++ b/src/Events/Program.cs @@ -22,8 +22,8 @@ public class Program return e.Level >= globalSettings.MinLogLevel.EventsSettings.IdentityToken; } - if (e.Properties.ContainsKey("RequestPath") && - !string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) && + if (e.Properties.TryGetValue("RequestPath", out var requestPath) && + !string.IsNullOrWhiteSpace(requestPath?.ToString()) && (context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer"))) { return false; diff --git a/src/Icons/Services/DomainMappingService.cs b/src/Icons/Services/DomainMappingService.cs index b41d48233f..21254b6d73 100644 --- a/src/Icons/Services/DomainMappingService.cs +++ b/src/Icons/Services/DomainMappingService.cs @@ -13,9 +13,9 @@ public class DomainMappingService : IDomainMappingService public string MapDomain(string hostname) { - if (_map.ContainsKey(hostname)) + if (_map.TryGetValue(hostname, out var mappedDomain)) { - return _map[hostname]; + return mappedDomain; } return hostname; diff --git a/src/Identity/Billing/Controller/AccountsController.cs b/src/Identity/Billing/Controller/AccountsController.cs index b83940d3aa..f476e4e094 100644 --- a/src/Identity/Billing/Controller/AccountsController.cs +++ b/src/Identity/Billing/Controller/AccountsController.cs @@ -1,11 +1,7 @@ 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; using Bit.Core.Utilities; using Bit.SharedWeb.Utilities; using Microsoft.AspNetCore.Mvc; @@ -15,9 +11,7 @@ namespace Bit.Identity.Billing.Controller; [Route("accounts")] [ExceptionHandlerFilter] public class AccountsController( - ICurrentContext currentContext, ISendTrialInitiationEmailForRegistrationCommand sendTrialInitiationEmailForRegistrationCommand, - IReferenceEventService referenceEventService, IFeatureService featureService) : Microsoft.AspNetCore.Mvc.Controller { [HttpPost("trial/send-verification-email")] @@ -36,15 +30,6 @@ public class AccountsController( model.Products, trialLength); - var refEvent = new ReferenceEvent - { - Type = ReferenceEventType.SignupEmailSubmit, - ClientId = currentContext.ClientId, - ClientVersion = currentContext.ClientVersion, - Source = ReferenceEventSource.Registration - }; - await referenceEventService.RaiseEventAsync(refEvent); - if (token != null) { return Ok(token); diff --git a/src/Identity/Controllers/AccountsController.cs b/src/Identity/Controllers/AccountsController.cs index 80e9536ea3..4965046bfc 100644 --- a/src/Identity/Controllers/AccountsController.cs +++ b/src/Identity/Controllers/AccountsController.cs @@ -16,9 +16,6 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tokens; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Bit.Identity.Models.Request.Accounts; using Bit.Identity.Models.Response.Accounts; @@ -39,7 +36,6 @@ public class AccountsController : Controller private readonly IDataProtectorTokenFactory _assertionOptionsDataProtector; private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand; private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand; - private readonly IReferenceEventService _referenceEventService; private readonly IFeatureService _featureService; private readonly IDataProtectorTokenFactory _registrationEmailVerificationTokenDataFactory; @@ -86,7 +82,6 @@ public class AccountsController : Controller IDataProtectorTokenFactory assertionOptionsDataProtector, IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand, ISendVerificationEmailForRegistrationCommand sendVerificationEmailForRegistrationCommand, - IReferenceEventService referenceEventService, IFeatureService featureService, IDataProtectorTokenFactory registrationEmailVerificationTokenDataFactory, GlobalSettings globalSettings @@ -99,7 +94,6 @@ public class AccountsController : Controller _assertionOptionsDataProtector = assertionOptionsDataProtector; _getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand; _sendVerificationEmailForRegistrationCommand = sendVerificationEmailForRegistrationCommand; - _referenceEventService = referenceEventService; _featureService = featureService; _registrationEmailVerificationTokenDataFactory = registrationEmailVerificationTokenDataFactory; @@ -115,15 +109,6 @@ public class AccountsController : Controller var token = await _sendVerificationEmailForRegistrationCommand.Run(model.Email, model.Name, model.ReceiveMarketingEmails); - var refEvent = new ReferenceEvent - { - Type = ReferenceEventType.SignupEmailSubmit, - ClientId = _currentContext.ClientId, - ClientVersion = _currentContext.ClientVersion, - Source = ReferenceEventSource.Registration - }; - await _referenceEventService.RaiseEventAsync(refEvent); - if (token != null) { return Ok(token); @@ -142,18 +127,6 @@ public class AccountsController : Controller var user = await _userRepository.GetByEmailAsync(model.Email); var userExists = user != null; - var refEvent = new ReferenceEvent - { - Type = ReferenceEventType.SignupEmailClicked, - ClientId = _currentContext.ClientId, - ClientVersion = _currentContext.ClientVersion, - Source = ReferenceEventSource.Registration, - EmailVerificationTokenValid = tokenValid, - UserAlreadyExists = userExists - }; - - await _referenceEventService.RaiseEventAsync(refEvent); - if (!tokenValid || userExists) { throw new BadRequestException("Expired link. Please restart registration or try logging in. You may already have an account"); diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index 45c0c26b17..dd4592aa0d 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -193,7 +193,7 @@ public abstract class BaseRequestValidator where T : class protected async Task FailAuthForLegacyUserAsync(User user, T context) { await BuildErrorResultAsync( - $"Encryption key migration is required. Please log in to the web vault at {_globalSettings.BaseServiceUri.VaultWithHash}", + $"Legacy encryption without a userkey is no longer supported. To recover your account, please contact support", false, context, user); } diff --git a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs index 6f2d81bd1b..7d468fafa8 100644 --- a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs @@ -27,6 +27,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator _userManager; private readonly IUpdateInstallationCommand _updateInstallationCommand; + private readonly Version _denyLegacyUserMinimumVersion = new(Constants.DenyLegacyUserMinimumVersion); public CustomTokenRequestValidator( UserManager userManager, @@ -73,7 +74,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator= _denyLegacyUserMinimumVersion)) { await FailAuthForLegacyUserAsync(null, context); return; diff --git a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs index 000f98c006..e4c1ebd15e 100644 --- a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs @@ -240,7 +240,7 @@ public class TwoFactorAuthenticationValidator( private bool OrgUsing2fa(IDictionary orgAbilities, Guid orgId) { - return orgAbilities != null && orgAbilities.ContainsKey(orgId) && - orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa; + return orgAbilities != null && orgAbilities.TryGetValue(orgId, out var orgAbility) && + orgAbility.Enabled && orgAbility.Using2fa; } } diff --git a/src/Identity/Startup.cs b/src/Identity/Startup.cs index 2d8ca55def..baaf9385af 100644 --- a/src/Identity/Startup.cs +++ b/src/Identity/Startup.cs @@ -120,9 +120,9 @@ public class Startup // Pass domain_hint onto the sso idp context.ProtocolMessage.DomainHint = context.Properties.Items["domain_hint"]; context.ProtocolMessage.Parameters.Add("organizationId", context.Properties.Items["organizationId"]); - if (context.Properties.Items.ContainsKey("user_identifier")) + if (context.Properties.Items.TryGetValue("user_identifier", out var userIdentifier)) { - context.ProtocolMessage.SessionState = context.Properties.Items["user_identifier"]; + context.ProtocolMessage.SessionState = userIdentifier; } if (context.Properties.Parameters.Count > 0 && diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 247d4c5d43..ccf2b5212f 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -352,15 +352,6 @@ public static class ServiceCollectionExtensions { services.AddSingleton(); } - - if (globalSettings.SelfHosted) - { - services.AddSingleton(); - } - else - { - services.AddSingleton(); - } } public static void AddOosServices(this IServiceCollection services) @@ -559,7 +550,8 @@ public static class ServiceCollectionExtensions if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.EventTopicName)) { - services.AddKeyedSingleton("broadcast"); + services.AddSingleton(); + services.AddKeyedSingleton("broadcast"); } else { @@ -572,7 +564,8 @@ public static class ServiceCollectionExtensions if (IsRabbitMqEnabled(globalSettings)) { - services.AddKeyedSingleton("broadcast"); + services.AddSingleton(); + services.AddKeyedSingleton("broadcast"); } else { @@ -594,13 +587,15 @@ public static class ServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddKeyedSingleton("persistent"); - services.AddSingleton(provider => new AzureServiceBusEventListenerService( handler: provider.GetRequiredService(), - logger: provider.GetRequiredService>(), + serviceBusService: provider.GetRequiredService(), + subscriptionName: globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName, globalSettings: globalSettings, - subscriptionName: globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName)); + logger: provider.GetRequiredService>() + ) + ); return services; } @@ -616,12 +611,10 @@ public static class ServiceCollectionExtensions { var routingKey = integrationType.ToRoutingKey(); - services.AddSingleton(); - services.AddKeyedSingleton(routingKey, (provider, _) => new EventIntegrationHandler( integrationType, - provider.GetRequiredService(), + provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService())); @@ -629,18 +622,22 @@ public static class ServiceCollectionExtensions services.AddSingleton(provider => new AzureServiceBusEventListenerService( handler: provider.GetRequiredKeyedService(routingKey), - logger: provider.GetRequiredService>(), + serviceBusService: provider.GetRequiredService(), + subscriptionName: eventSubscriptionName, globalSettings: globalSettings, - subscriptionName: eventSubscriptionName)); + logger: provider.GetRequiredService>() + ) + ); services.AddSingleton, THandler>(); - services.AddSingleton(provider => new AzureServiceBusIntegrationListenerService( handler: provider.GetRequiredService>(), + topicName: globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName, subscriptionName: integrationSubscriptionName, - logger: provider.GetRequiredService>(), - globalSettings: globalSettings)); + maxRetries: globalSettings.EventLogging.AzureServiceBus.MaxRetries, + serviceBusService: provider.GetRequiredService(), + logger: provider.GetRequiredService>())); return services; } @@ -651,6 +648,8 @@ public static class ServiceCollectionExtensions !CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.EventTopicName)) return services; + services.AddSingleton(); + services.AddSingleton(); services.AddAzureServiceBusEventRepositoryListener(globalSettings); services.AddSlackService(globalSettings); @@ -677,9 +676,9 @@ public static class ServiceCollectionExtensions services.AddSingleton(provider => new RabbitMqEventListenerService( provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName)); + globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName, + provider.GetRequiredService(), + provider.GetRequiredService>())); return services; } @@ -688,19 +687,17 @@ public static class ServiceCollectionExtensions string eventQueueName, string integrationQueueName, string integrationRetryQueueName, - string integrationDeadLetterQueueName, - IntegrationType integrationType, - GlobalSettings globalSettings) + int maxRetries, + IntegrationType integrationType) where TConfig : class where THandler : class, IIntegrationHandler { var routingKey = integrationType.ToRoutingKey(); - services.AddSingleton(); services.AddKeyedSingleton(routingKey, (provider, _) => new EventIntegrationHandler( integrationType, - provider.GetRequiredService(), + provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService())); @@ -708,9 +705,9 @@ public static class ServiceCollectionExtensions services.AddSingleton(provider => new RabbitMqEventListenerService( provider.GetRequiredKeyedService(routingKey), - provider.GetRequiredService>(), - globalSettings, - eventQueueName)); + eventQueueName, + provider.GetRequiredService(), + provider.GetRequiredService>())); services.AddSingleton, THandler>(); services.AddSingleton(provider => @@ -719,8 +716,8 @@ public static class ServiceCollectionExtensions routingKey: routingKey, queueName: integrationQueueName, retryQueueName: integrationRetryQueueName, - deadLetterQueueName: integrationDeadLetterQueueName, - globalSettings: globalSettings, + maxRetries: maxRetries, + rabbitMqService: provider.GetRequiredService(), logger: provider.GetRequiredService>())); return services; @@ -733,6 +730,8 @@ public static class ServiceCollectionExtensions return services; } + services.AddSingleton(); + services.AddSingleton(); services.AddRabbitMqEventRepositoryListener(globalSettings); services.AddSlackService(globalSettings); @@ -740,18 +739,16 @@ public static class ServiceCollectionExtensions globalSettings.EventLogging.RabbitMq.SlackEventsQueueName, globalSettings.EventLogging.RabbitMq.SlackIntegrationQueueName, globalSettings.EventLogging.RabbitMq.SlackIntegrationRetryQueueName, - globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName, - IntegrationType.Slack, - globalSettings); + globalSettings.EventLogging.RabbitMq.MaxRetries, + IntegrationType.Slack); services.AddHttpClient(WebhookIntegrationHandler.HttpClientName); services.AddRabbitMqIntegration( globalSettings.EventLogging.RabbitMq.WebhookEventsQueueName, globalSettings.EventLogging.RabbitMq.WebhookIntegrationQueueName, globalSettings.EventLogging.RabbitMq.WebhookIntegrationRetryQueueName, - globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName, - IntegrationType.Webhook, - globalSettings); + globalSettings.EventLogging.RabbitMq.MaxRetries, + IntegrationType.Webhook); return services; } diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs index 94432b05a0..d77a41f52e 100644 --- a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs @@ -14,7 +14,7 @@ public class OrganizationUsersControllerPerformanceTest(ITestOutputHelper testOu [InlineData(60000)] public async Task GetAsync(int seats) { - await using var factory = new ApiApplicationFactory(); + await using var factory = new SqlServerApiApplicationFactory(); var client = factory.CreateClient(); var db = factory.GetDatabaseContext(); diff --git a/test/Api.IntegrationTest/Factories/ApiApplicationFactory.cs b/test/Api.IntegrationTest/Factories/ApiApplicationFactory.cs index a0963745de..08c5973936 100644 --- a/test/Api.IntegrationTest/Factories/ApiApplicationFactory.cs +++ b/test/Api.IntegrationTest/Factories/ApiApplicationFactory.cs @@ -1,10 +1,11 @@ using Bit.Core; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Enums; +using Bit.IntegrationTestCommon; using Bit.IntegrationTestCommon.Factories; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.TestHost; -using Microsoft.Data.Sqlite; +using Xunit; #nullable enable @@ -12,16 +13,19 @@ namespace Bit.Api.IntegrationTest.Factories; public class ApiApplicationFactory : WebApplicationFactoryBase { - private readonly IdentityApplicationFactory _identityApplicationFactory; - private const string _connectionString = "DataSource=:memory:"; + protected IdentityApplicationFactory _identityApplicationFactory; - public ApiApplicationFactory() + public ApiApplicationFactory() : this(new SqliteTestDatabase()) { - SqliteConnection = new SqliteConnection(_connectionString); - SqliteConnection.Open(); + } + + protected ApiApplicationFactory(ITestDatabase db) + { + TestDatabase = db; _identityApplicationFactory = new IdentityApplicationFactory(); - _identityApplicationFactory.SqliteConnection = SqliteConnection; + _identityApplicationFactory.TestDatabase = TestDatabase; + _identityApplicationFactory.ManagesDatabase = false; } protected override void ConfigureWebHost(IWebHostBuilder builder) @@ -47,6 +51,10 @@ public class ApiApplicationFactory : WebApplicationFactoryBase public async Task<(string Token, string RefreshToken)> LoginWithNewAccount( string email = "integration-test@bitwarden.com", string masterPasswordHash = "master_password_hash") { + // This might be the first action in a test and since it forwards to the Identity server, we need to ensure that + // this server is initialized since it's responsible for seeding the database. + Assert.NotNull(Services); + await _identityApplicationFactory.RegisterNewIdentityFactoryUserAsync( new RegisterFinishRequestModel { @@ -73,12 +81,6 @@ public class ApiApplicationFactory : WebApplicationFactoryBase return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash); } - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - SqliteConnection!.Dispose(); - } - /// /// Helper for logging in via client secret. /// Currently used for Secrets Manager service accounts diff --git a/test/Api.IntegrationTest/Factories/SqlServerApiApplicationFactory.cs b/test/Api.IntegrationTest/Factories/SqlServerApiApplicationFactory.cs new file mode 100644 index 0000000000..b8cf721232 --- /dev/null +++ b/test/Api.IntegrationTest/Factories/SqlServerApiApplicationFactory.cs @@ -0,0 +1,7 @@ +using Bit.IntegrationTestCommon; + +#nullable enable + +namespace Bit.Api.IntegrationTest.Factories; + +public class SqlServerApiApplicationFactory() : ApiApplicationFactory(new SqlServerTestDatabase()); diff --git a/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs index 16e32870ad..63afb2a7a8 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs @@ -23,7 +23,6 @@ using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Tools.Services; using NSubstitute; using NSubstitute.ReturnsExtensions; using Xunit; @@ -46,7 +45,6 @@ public class OrganizationsControllerTests : IDisposable private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand; private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand; - private readonly IReferenceEventService _referenceEventService; private readonly ISubscriberService _subscriberService; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IOrganizationInstallationRepository _organizationInstallationRepository; @@ -71,7 +69,6 @@ public class OrganizationsControllerTests : IDisposable _updateSecretsManagerSubscriptionCommand = Substitute.For(); _upgradeOrganizationPlanCommand = Substitute.For(); _addSecretsManagerSubscriptionCommand = Substitute.For(); - _referenceEventService = Substitute.For(); _subscriberService = Substitute.For(); _removeOrganizationUserCommand = Substitute.For(); _organizationInstallationRepository = Substitute.For(); @@ -90,7 +87,6 @@ public class OrganizationsControllerTests : IDisposable _updateSecretsManagerSubscriptionCommand, _upgradeOrganizationPlanCommand, _addSecretsManagerSubscriptionCommand, - _referenceEventService, _subscriberService, _organizationInstallationRepository, _pricingClient); diff --git a/test/Api.Test/Tools/Controllers/SendsControllerTests.cs b/test/Api.Test/Tools/Controllers/SendsControllerTests.cs index b1fa5c9260..7210bddebb 100644 --- a/test/Api.Test/Tools/Controllers/SendsControllerTests.cs +++ b/test/Api.Test/Tools/Controllers/SendsControllerTests.cs @@ -3,7 +3,6 @@ using AutoFixture.Xunit2; using Bit.Api.Tools.Controllers; using Bit.Api.Tools.Models.Request; using Bit.Api.Tools.Models.Response; -using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Services; @@ -33,7 +32,6 @@ public class SendsControllerTests : IDisposable private readonly ISendAuthorizationService _sendAuthorizationService; private readonly ISendFileStorageService _sendFileStorageService; private readonly ILogger _logger; - private readonly ICurrentContext _currentContext; public SendsControllerTests() { @@ -45,7 +43,6 @@ public class SendsControllerTests : IDisposable _sendFileStorageService = Substitute.For(); _globalSettings = new GlobalSettings(); _logger = Substitute.For>(); - _currentContext = Substitute.For(); _sut = new SendsController( _sendRepository, @@ -55,8 +52,7 @@ public class SendsControllerTests : IDisposable _nonAnonymousSendCommand, _sendFileStorageService, _logger, - _globalSettings, - _currentContext + _globalSettings ); } diff --git a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs index e4643f3185..bca6bbc048 100644 --- a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs @@ -1,6 +1,7 @@ using System.Security.Claims; using System.Text.Json; using Bit.Api.Vault.Controllers; +using Bit.Api.Vault.Models; using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Response; using Bit.Core; @@ -1741,8 +1742,200 @@ public class CiphersControllerTests { model.OrganizationId = Guid.NewGuid(); - sutProvider.GetDependency().ProviderUserForOrgAsync(model.OrganizationId).Returns(true); + sutProvider.GetDependency() + .ProviderUserForOrgAsync(new Guid(model.OrganizationId.ToString())) + .Returns(Task.FromResult(true)); - await Assert.ThrowsAsync(() => sutProvider.Sut.PutRestoreManyAdmin(model)); + await Assert.ThrowsAsync( + () => sutProvider.Sut.PutRestoreManyAdmin(model) + ); } + + [Theory] + [BitAutoData] + public async Task PutShareMany_ShouldShareCiphersAndReturnRevisionDateMap( + User user, + Guid organizationId, + Guid userId, + SutProvider sutProvider) + { + var oldDate1 = DateTime.UtcNow.AddDays(-1); + var oldDate2 = DateTime.UtcNow.AddDays(-2); + var detail1 = new CipherDetails + { + Id = Guid.NewGuid(), + UserId = userId, + OrganizationId = organizationId, + Type = CipherType.Login, + Data = JsonSerializer.Serialize(new CipherLoginData()), + RevisionDate = oldDate1 + }; + var detail2 = new CipherDetails + { + Id = Guid.NewGuid(), + UserId = userId, + OrganizationId = organizationId, + Type = CipherType.Login, + Data = JsonSerializer.Serialize(new CipherLoginData()), + RevisionDate = oldDate2 + }; + var preloadedDetails = new List { detail1, detail2 }; + + var newDate1 = oldDate1.AddMinutes(5); + var newDate2 = oldDate2.AddMinutes(5); + var updatedCipher1 = new CipherDetails { Id = detail1.Id, RevisionDate = newDate1, Type = detail1.Type, Data = detail1.Data }; + var updatedCipher2 = new CipherDetails { Id = detail2.Id, RevisionDate = newDate2, Type = detail2.Type, Data = detail2.Data }; + + sutProvider.GetDependency() + .OrganizationUser(organizationId) + .Returns(Task.FromResult(true)); + sutProvider.GetDependency() + .GetUserByPrincipalAsync(Arg.Any()) + .Returns(Task.FromResult(user)); + sutProvider.GetDependency() + .GetProperUserId(default!) + .ReturnsForAnyArgs(userId); + + sutProvider.GetDependency() + .GetManyByUserIdAsync(userId, withOrganizations: false) + .Returns(Task.FromResult((ICollection)preloadedDetails)); + + sutProvider.GetDependency() + .ShareManyAsync( + Arg.Any>(), + organizationId, + Arg.Any>(), + userId + ) + .Returns(Task.FromResult>(new[] { updatedCipher1, updatedCipher2 })); + + var cipherRequests = preloadedDetails.Select(d => + { + var m = new CipherWithIdRequestModel + { + Id = d.Id, + OrganizationId = d.OrganizationId!.Value.ToString(), + LastKnownRevisionDate = d.RevisionDate, + Type = d.Type, + }; + + if (d.Type == CipherType.Login) + { + m.Login = new CipherLoginModel + { + Username = "", + Password = "", + Uris = [], + }; + m.Name = ""; + m.Notes = ""; + m.Fields = Array.Empty(); + m.PasswordHistory = Array.Empty(); + } + + // similar for SecureNote, Card, etc., if you ever hit those branches + return m; + }).ToList(); + + var model = new CipherBulkShareRequestModel + { + Ciphers = cipherRequests, + CollectionIds = new[] { Guid.NewGuid().ToString() } + }; + + var result = await sutProvider.Sut.PutShareMany(model); + + Assert.Equal(2, result.Data.Count()); + var revisionDates = result.Data.Select(x => x.RevisionDate).ToList(); + Assert.Contains(newDate1, revisionDates); + Assert.Contains(newDate2, revisionDates); + + await sutProvider.GetDependency() + .Received(1) + .ShareManyAsync( + Arg.Is>(list => + list.Select(x => x.Item1.Id).OrderBy(id => id) + .SequenceEqual(new[] { detail1.Id, detail2.Id }.OrderBy(id => id)) + ), + organizationId, + Arg.Any>(), + userId + ); + } + + [Theory, BitAutoData] + public async Task PutShareMany_OrganizationUserFalse_ThrowsNotFound( + CipherBulkShareRequestModel model, + SutProvider sut) + { + model.Ciphers = new[] { + new CipherWithIdRequestModel { Id = Guid.NewGuid(), OrganizationId = Guid.NewGuid().ToString() } + }; + sut.GetDependency() + .OrganizationUser(Arg.Any()) + .Returns(Task.FromResult(false)); + + await Assert.ThrowsAsync(() => sut.Sut.PutShareMany(model)); + } + [Theory, BitAutoData] + public async Task PutShareMany_CipherNotOwned_ThrowsNotFoundException( + Guid organizationId, + Guid userId, + CipherWithIdRequestModel request, + SutProvider sutProvider) + { + request.EncryptedFor = userId; + var model = new CipherBulkShareRequestModel + { + Ciphers = new[] { request }, + CollectionIds = new[] { Guid.NewGuid().ToString() } + }; + + sutProvider.GetDependency() + .OrganizationUser(organizationId) + .Returns(Task.FromResult(true)); + sutProvider.GetDependency() + .GetProperUserId(default) + .ReturnsForAnyArgs(userId); + sutProvider.GetDependency() + .GetManyByUserIdAsync(userId, withOrganizations: false) + .Returns(Task.FromResult((ICollection)new List())); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.PutShareMany(model) + ); + } + + [Theory, BitAutoData] + public async Task PutShareMany_EncryptedForWrongUser_ThrowsNotFoundException( + Guid organizationId, + Guid userId, + CipherWithIdRequestModel request, + SutProvider sutProvider) + { + request.EncryptedFor = Guid.NewGuid(); // not equal to userId + var model = new CipherBulkShareRequestModel + { + Ciphers = new[] { request }, + CollectionIds = new[] { Guid.NewGuid().ToString() } + }; + + sutProvider.GetDependency() + .OrganizationUser(organizationId) + .Returns(Task.FromResult(true)); + sutProvider.GetDependency() + .GetProperUserId(default) + .ReturnsForAnyArgs(userId); + + var existing = new CipherDetails { Id = request.Id.Value }; + sutProvider.GetDependency() + .GetManyByUserIdAsync(userId, withOrganizations: false) + .Returns(Task.FromResult((ICollection)(new[] { existing }))); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.PutShareMany(model) + ); + } + } + diff --git a/test/Common/AutoFixture/SutProvider.cs b/test/Common/AutoFixture/SutProvider.cs index fefe6c3ebf..4b6f268ac3 100644 --- a/test/Common/AutoFixture/SutProvider.cs +++ b/test/Common/AutoFixture/SutProvider.cs @@ -27,9 +27,9 @@ public class SutProvider : ISutProvider => SetDependency(typeof(T), dependency, parameterName); public SutProvider SetDependency(Type dependencyType, object dependency, string parameterName = "") { - if (_dependencies.ContainsKey(dependencyType)) + if (_dependencies.TryGetValue(dependencyType, out var dependencyForType)) { - _dependencies[dependencyType][parameterName] = dependency; + dependencyForType[parameterName] = dependency; } else { @@ -46,12 +46,11 @@ public class SutProvider : ISutProvider { return _dependencies[dependencyType][parameterName]; } - else if (_dependencies.ContainsKey(dependencyType)) + else if (_dependencies.TryGetValue(dependencyType, out var knownDependencies)) { - var knownDependencies = _dependencies[dependencyType]; if (knownDependencies.Values.Count == 1) { - return _dependencies[dependencyType].Values.Single(); + return knownDependencies.Values.Single(); } else { diff --git a/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs b/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs index 0946841347..6ed84717de 100644 --- a/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs +++ b/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs @@ -7,12 +7,17 @@ namespace Bit.Core.Test.Models.Data.Integrations; public class IntegrationMessageTests { + private const string _messageId = "TestMessageId"; + [Fact] public void ApplyRetry_IncrementsRetryCountAndSetsDelayUntilDate() { var message = new IntegrationMessage { + Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"), + MessageId = _messageId, RetryCount = 2, + RenderedTemplate = string.Empty, DelayUntilDate = null }; @@ -30,19 +35,22 @@ public class IntegrationMessageTests var message = new IntegrationMessage { Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"), + MessageId = _messageId, RenderedTemplate = "This is the message", IntegrationType = IntegrationType.Webhook, RetryCount = 2, - DelayUntilDate = null + DelayUntilDate = DateTime.UtcNow }; var json = message.ToJson(); var result = IntegrationMessage.FromJson(json); Assert.Equal(message.Configuration, result.Configuration); + Assert.Equal(message.MessageId, result.MessageId); Assert.Equal(message.RenderedTemplate, result.RenderedTemplate); Assert.Equal(message.IntegrationType, result.IntegrationType); Assert.Equal(message.RetryCount, result.RetryCount); + Assert.Equal(message.DelayUntilDate, result.DelayUntilDate); } [Fact] @@ -51,4 +59,26 @@ public class IntegrationMessageTests var json = "{ Invalid JSON"; Assert.Throws(() => IntegrationMessage.FromJson(json)); } + + [Fact] + public void ToJson_BaseIntegrationMessage_DeserializesCorrectly() + { + var message = new IntegrationMessage + { + MessageId = _messageId, + RenderedTemplate = "This is the message", + IntegrationType = IntegrationType.Webhook, + RetryCount = 2, + DelayUntilDate = DateTime.UtcNow + }; + + var json = message.ToJson(); + var result = JsonSerializer.Deserialize(json); + + Assert.Equal(message.MessageId, result.MessageId); + Assert.Equal(message.RenderedTemplate, result.RenderedTemplate); + Assert.Equal(message.IntegrationType, result.IntegrationType); + Assert.Equal(message.RetryCount, result.RetryCount); + Assert.Equal(message.DelayUntilDate, result.DelayUntilDate); + } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Groups/CreateGroupCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Groups/CreateGroupCommandTests.cs index 20745673fd..1481ec6a91 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Groups/CreateGroupCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Groups/CreateGroupCommandTests.cs @@ -6,9 +6,6 @@ using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Services; using Bit.Core.Test.AutoFixture.OrganizationFixtures; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; @@ -27,7 +24,6 @@ public class CreateGroupCommandTests await sutProvider.GetDependency().Received(1).CreateAsync(group); await sutProvider.GetDependency().Received(1).LogGroupEventAsync(group, Enums.EventType.Group_Created); - await sutProvider.GetDependency().Received(1).RaiseEventAsync(Arg.Is(r => r.Type == ReferenceEventType.GroupCreated && r.Id == organization.Id && r.Source == ReferenceEventSource.Organization)); AssertHelper.AssertRecent(group.CreationDate); AssertHelper.AssertRecent(group.RevisionDate); } @@ -48,7 +44,6 @@ public class CreateGroupCommandTests await sutProvider.GetDependency().Received(1).CreateAsync(group, collections); await sutProvider.GetDependency().Received(1).LogGroupEventAsync(group, Enums.EventType.Group_Created); - await sutProvider.GetDependency().Received(1).RaiseEventAsync(Arg.Is(r => r.Type == ReferenceEventType.GroupCreated && r.Id == organization.Id && r.Source == ReferenceEventSource.Organization)); AssertHelper.AssertRecent(group.CreationDate); AssertHelper.AssertRecent(group.RevisionDate); } @@ -60,7 +55,6 @@ public class CreateGroupCommandTests await sutProvider.GetDependency().Received(1).CreateAsync(group); await sutProvider.GetDependency().Received(1).LogGroupEventAsync(group, Enums.EventType.Group_Created, eventSystemUser); - await sutProvider.GetDependency().Received(1).RaiseEventAsync(Arg.Is(r => r.Type == ReferenceEventType.GroupCreated && r.Id == organization.Id && r.Source == ReferenceEventSource.Organization)); AssertHelper.AssertRecent(group.CreationDate); AssertHelper.AssertRecent(group.RevisionDate); } @@ -74,7 +68,6 @@ public class CreateGroupCommandTests await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().LogGroupEventAsync(default, default, default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().RaiseEventAsync(default); } [Theory, OrganizationCustomize(UseGroups = false), BitAutoData] @@ -86,6 +79,5 @@ public class CreateGroupCommandTests await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().LogGroupEventAsync(default, default, default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().RaiseEventAsync(default); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs index 544c97d166..8acbe5eded 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs @@ -10,9 +10,6 @@ using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.Data; using Bit.Core.Repositories; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -51,15 +48,6 @@ public class CloudICloudOrganizationSignUpCommandTests await sutProvider.GetDependency().Received(1).CreateAsync( Arg.Is(o => o.AccessSecretsManager == signup.UseSecretsManager)); - await sutProvider.GetDependency().Received(1) - .RaiseEventAsync(Arg.Is(referenceEvent => - referenceEvent.Type == ReferenceEventType.Signup && - referenceEvent.PlanName == plan.Name && - referenceEvent.PlanType == plan.Type && - referenceEvent.Seats == result.Organization.Seats && - referenceEvent.Storage == result.Organization.MaxStorageGb)); - // TODO: add reference events for SmSeats and Service Accounts - see AC-1481 - Assert.NotNull(result.Organization); Assert.NotNull(result.OrganizationUser); @@ -145,15 +133,6 @@ public class CloudICloudOrganizationSignUpCommandTests await sutProvider.GetDependency().Received(1).CreateAsync( Arg.Is(o => o.AccessSecretsManager == signup.UseSecretsManager)); - await sutProvider.GetDependency().Received(1) - .RaiseEventAsync(Arg.Is(referenceEvent => - referenceEvent.Type == ReferenceEventType.Signup && - referenceEvent.PlanName == plan.Name && - referenceEvent.PlanType == plan.Type && - referenceEvent.Seats == result.Organization.Seats && - referenceEvent.Storage == result.Organization.MaxStorageGb)); - // TODO: add reference events for SmSeats and Service Accounts - see AC-1481 - Assert.NotNull(result.Organization); Assert.NotNull(result.OrganizationUser); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ProviderClientOrganizationSignUpCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ProviderClientOrganizationSignUpCommandTests.cs index b13c7e5b65..881f134b4c 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ProviderClientOrganizationSignUpCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ProviderClientOrganizationSignUpCommandTests.cs @@ -10,9 +10,6 @@ using Bit.Core.Models.Data; using Bit.Core.Models.StaticStore; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -65,17 +62,6 @@ public class ProviderClientOrganizationSignUpCommandTests ) ); - await sutProvider.GetDependency() - .Received(1) - .RaiseEventAsync(Arg.Is(referenceEvent => - referenceEvent.Type == ReferenceEventType.Signup && - referenceEvent.PlanName == plan.Name && - referenceEvent.PlanType == plan.Type && - referenceEvent.Seats == result.Organization.Seats && - referenceEvent.Storage == result.Organization.MaxStorageGb && - referenceEvent.SignupInitiationPath == signup.InitiationPath - )); - await sutProvider.GetDependency() .Received(1) .CreateAsync( diff --git a/test/Core.Test/AdminConsole/Services/AzureServiceBusEventListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/AzureServiceBusEventListenerServiceTests.cs new file mode 100644 index 0000000000..13704817ca --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/AzureServiceBusEventListenerServiceTests.cs @@ -0,0 +1,133 @@ +using System.Text.Json; +using Azure.Messaging.ServiceBus; +using Bit.Core.Models.Data; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class AzureServiceBusEventListenerServiceTests +{ + private readonly IEventMessageHandler _handler = Substitute.For(); + private readonly ILogger _logger = + Substitute.For>(); + private const string _messageId = "messageId"; + + private SutProvider GetSutProvider() + { + return new SutProvider() + .SetDependency(_handler) + .SetDependency(_logger) + .SetDependency("test-subscription", "subscriptionName") + .Create(); + } + + [Theory, BitAutoData] + public async Task ProcessErrorAsync_LogsError(ProcessErrorEventArgs args) + { + var sutProvider = GetSutProvider(); + + await sutProvider.Sut.ProcessErrorAsync(args); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + + [Fact] + public async Task ProcessReceivedMessageAsync_EmptyJson_LogsError() + { + var sutProvider = GetSutProvider(); + await sutProvider.Sut.ProcessReceivedMessageAsync(string.Empty, _messageId); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + + [Fact] + public async Task ProcessReceivedMessageAsync_InvalidJson_LogsError() + { + var sutProvider = GetSutProvider(); + await sutProvider.Sut.ProcessReceivedMessageAsync("{ Inavlid JSON }", _messageId); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Is(o => o.ToString().Contains("Invalid JSON")), + Arg.Any(), + Arg.Any>()); + } + + [Fact] + public async Task ProcessReceivedMessageAsync_InvalidJsonArray_LogsError() + { + var sutProvider = GetSutProvider(); + await sutProvider.Sut.ProcessReceivedMessageAsync( + "{ \"not a valid\", \"list of event messages\" }", + _messageId + ); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + + [Fact] + public async Task ProcessReceivedMessageAsync_InvalidJsonObject_LogsError() + { + var sutProvider = GetSutProvider(); + await sutProvider.Sut.ProcessReceivedMessageAsync( + JsonSerializer.Serialize(DateTime.UtcNow), // wrong object - not EventMessage + _messageId + ); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + + [Theory, BitAutoData] + public async Task ProcessReceivedMessageAsync_SingleEvent_DelegatesToHandler(EventMessage message) + { + var sutProvider = GetSutProvider(); + await sutProvider.Sut.ProcessReceivedMessageAsync( + JsonSerializer.Serialize(message), + _messageId + ); + + await sutProvider.GetDependency().Received(1).HandleEventAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(message, new[] { "IdempotencyId" }))); + } + + [Theory, BitAutoData] + public async Task ProcessReceivedMessageAsync_ManyEvents_DelegatesToHandler(IEnumerable messages) + { + var sutProvider = GetSutProvider(); + await sutProvider.Sut.ProcessReceivedMessageAsync( + JsonSerializer.Serialize(messages), + _messageId + ); + + await sutProvider.GetDependency().Received(1).HandleManyEventsAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(messages, new[] { "IdempotencyId" }))); + } +} diff --git a/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs new file mode 100644 index 0000000000..b1eb117cf0 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs @@ -0,0 +1,124 @@ +#nullable enable + +using Azure.Messaging.ServiceBus; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class AzureServiceBusIntegrationListenerServiceTests +{ + private const int _maxRetries = 3; + private const string _topicName = "test_topic"; + private const string _subscriptionName = "test_subscription"; + private readonly IIntegrationHandler _handler = Substitute.For(); + private readonly IAzureServiceBusService _serviceBusService = Substitute.For(); + private readonly ILogger _logger = + Substitute.For>(); + + private SutProvider GetSutProvider() + { + return new SutProvider() + .SetDependency(_handler) + .SetDependency(_serviceBusService) + .SetDependency(_topicName, "topicName") + .SetDependency(_subscriptionName, "subscriptionName") + .SetDependency(_maxRetries, "maxRetries") + .SetDependency(_logger) + .Create(); + } + + [Theory, BitAutoData] + public async Task ProcessErrorAsync_LogsError(ProcessErrorEventArgs args) + { + var sutProvider = GetSutProvider(); + await sutProvider.Sut.ProcessErrorAsync(args); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + + [Theory, BitAutoData] + public async Task HandleMessageAsync_FailureNotRetryable_PublishesToDeadLetterQueue(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + message.RetryCount = 0; + + var result = new IntegrationHandlerResult(false, message); + result.Retryable = false; + _handler.HandleAsync(Arg.Any()).Returns(result); + + var expected = (IntegrationMessage)IntegrationMessage.FromJson(message.ToJson())!; + + Assert.False(await sutProvider.Sut.HandleMessageAsync(message.ToJson())); + + await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); + await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(default); + } + + [Theory, BitAutoData] + public async Task HandleMessageAsync_FailureRetryableButTooManyRetries_PublishesToDeadLetterQueue(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + message.RetryCount = _maxRetries; + var result = new IntegrationHandlerResult(false, message); + result.Retryable = true; + + _handler.HandleAsync(Arg.Any()).Returns(result); + + var expected = (IntegrationMessage)IntegrationMessage.FromJson(message.ToJson())!; + + Assert.False(await sutProvider.Sut.HandleMessageAsync(message.ToJson())); + + await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); + await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(default); + } + + [Theory, BitAutoData] + public async Task HandleMessageAsync_FailureRetryable_PublishesToRetryQueue(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + message.RetryCount = 0; + + var result = new IntegrationHandlerResult(false, message); + result.Retryable = true; + result.DelayUntilDate = DateTime.UtcNow.AddMinutes(1); + _handler.HandleAsync(Arg.Any()).Returns(result); + + var expected = (IntegrationMessage)IntegrationMessage.FromJson(message.ToJson())!; + + Assert.True(await sutProvider.Sut.HandleMessageAsync(message.ToJson())); + + await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); + await _serviceBusService.Received(1).PublishToRetryAsync(message); + } + + [Theory, BitAutoData] + public async Task HandleMessageAsync_SuccessfulResult_Succeeds(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + var result = new IntegrationHandlerResult(true, message); + _handler.HandleAsync(Arg.Any()).Returns(result); + + var expected = (IntegrationMessage)IntegrationMessage.FromJson(message.ToJson())!; + + Assert.True(await sutProvider.Sut.HandleMessageAsync(message.ToJson())); + + await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); + await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(default); + } +} diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationEventWriteServiceTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationEventWriteServiceTests.cs new file mode 100644 index 0000000000..9369690d86 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationEventWriteServiceTests.cs @@ -0,0 +1,57 @@ +using System.Text.Json; +using Bit.Core.Models.Data; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class EventIntegrationEventWriteServiceTests +{ + private readonly IEventIntegrationPublisher _eventIntegrationPublisher = Substitute.For(); + private readonly EventIntegrationEventWriteService Subject; + + public EventIntegrationEventWriteServiceTests() + { + Subject = new EventIntegrationEventWriteService(_eventIntegrationPublisher); + } + + [Theory, BitAutoData] + public async Task CreateAsync_EventPublishedToEventQueue(EventMessage eventMessage) + { + var expected = JsonSerializer.Serialize(eventMessage); + await Subject.CreateAsync(eventMessage); + await _eventIntegrationPublisher.Received(1).PublishEventAsync( + Arg.Is(body => AssertJsonStringsMatch(eventMessage, body))); + } + + [Theory, BitAutoData] + public async Task CreateManyAsync_EventsPublishedToEventQueue(IEnumerable eventMessages) + { + await Subject.CreateManyAsync(eventMessages); + await _eventIntegrationPublisher.Received(1).PublishEventAsync( + Arg.Is(body => AssertJsonStringsMatch(eventMessages, body))); + } + + private static bool AssertJsonStringsMatch(EventMessage expected, string body) + { + var actual = JsonSerializer.Deserialize(body); + AssertHelper.AssertPropertyEqual(expected, actual, new[] { "IdempotencyId" }); + return true; + } + + private static bool AssertJsonStringsMatch(IEnumerable expected, string body) + { + using var actual = JsonSerializer.Deserialize>(body).GetEnumerator(); + + foreach (var expectedMessage in expected) + { + actual.MoveNext(); + AssertHelper.AssertPropertyEqual(expectedMessage, actual.Current, new[] { "IdempotencyId" }); + } + return true; + } +} diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs index f0a0d1d724..0962df52cd 100644 --- a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs @@ -24,7 +24,7 @@ public class EventIntegrationHandlerTests private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#"; private const string _url = "https://localhost"; private const string _url2 = "https://example.com"; - private readonly IIntegrationPublisher _integrationPublisher = Substitute.For(); + private readonly IEventIntegrationPublisher _eventIntegrationPublisher = Substitute.For(); private SutProvider> GetSutProvider( List configurations) @@ -35,7 +35,7 @@ public class EventIntegrationHandlerTests return new SutProvider>() .SetDependency(configurationRepository) - .SetDependency(_integrationPublisher) + .SetDependency(_eventIntegrationPublisher) .SetDependency(IntegrationType.Webhook) .Create(); } @@ -45,6 +45,7 @@ public class EventIntegrationHandlerTests return new IntegrationMessage() { IntegrationType = IntegrationType.Webhook, + MessageId = "TestMessageId", Configuration = new WebhookIntegrationConfigurationDetails(_url), RenderedTemplate = template, RetryCount = 0, @@ -87,7 +88,7 @@ public class EventIntegrationHandlerTests var sutProvider = GetSutProvider(NoConfigurations()); await sutProvider.Sut.HandleEventAsync(eventMessage); - Assert.Empty(_integrationPublisher.ReceivedCalls()); + Assert.Empty(_eventIntegrationPublisher.ReceivedCalls()); } [Theory, BitAutoData] @@ -101,8 +102,9 @@ public class EventIntegrationHandlerTests $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); - Assert.Single(_integrationPublisher.ReceivedCalls()); - await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); } @@ -120,8 +122,9 @@ public class EventIntegrationHandlerTests var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}"); - Assert.Single(_integrationPublisher.ReceivedCalls()); - await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.ActingUserId ?? Guid.Empty); } @@ -136,12 +139,13 @@ public class EventIntegrationHandlerTests sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(organization); await sutProvider.Sut.HandleEventAsync(eventMessage); - Assert.Single(_integrationPublisher.ReceivedCalls()); + Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"Org: {organization.Name}"); - Assert.Single(_integrationPublisher.ReceivedCalls()); - await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); } @@ -159,8 +163,9 @@ public class EventIntegrationHandlerTests var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}"); - Assert.Single(_integrationPublisher.ReceivedCalls()); - await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty); } @@ -171,7 +176,7 @@ public class EventIntegrationHandlerTests var sutProvider = GetSutProvider(NoConfigurations()); await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - Assert.Empty(_integrationPublisher.ReceivedCalls()); + Assert.Empty(_eventIntegrationPublisher.ReceivedCalls()); } [Theory, BitAutoData] @@ -186,7 +191,8 @@ public class EventIntegrationHandlerTests var expectedMessage = EventIntegrationHandlerTests.expectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); - await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); } } @@ -203,10 +209,12 @@ public class EventIntegrationHandlerTests var expectedMessage = EventIntegrationHandlerTests.expectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); - await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_url2); - await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); } } } diff --git a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs index 10f39665d5..10e42c92cc 100644 --- a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs @@ -15,6 +15,7 @@ public class IntegrationHandlerTests var expected = new IntegrationMessage() { Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"), + MessageId = "TestMessageId", IntegrationType = IntegrationType.Webhook, RenderedTemplate = "Template", DelayUntilDate = null, diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index 18f1f79900..d926e282c9 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -22,9 +22,6 @@ using Bit.Core.Settings; using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; using Bit.Core.Tokens; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -100,10 +97,6 @@ public class OrganizationServiceTests await sutProvider.GetDependency().Received(1) .LogOrganizationUserEventsAsync(Arg.Is>(events => events.Count() == expectedNewUsersCount)); - await sutProvider.GetDependency().Received(1) - .RaiseEventAsync(Arg.Is(referenceEvent => - referenceEvent.Type == ReferenceEventType.InvitedUsers && referenceEvent.Id == org.Id && - referenceEvent.Users == expectedNewUsersCount)); } [Theory, PaidOrganizationCustomize, BitAutoData] @@ -170,10 +163,6 @@ public class OrganizationServiceTests await sutProvider.GetDependency().Received(1) .LogOrganizationUserEventsAsync(Arg.Is>(events => events.Count(e => e.Item2 == EventType.OrganizationUser_Invited) == expectedNewUsersCount)); - await sutProvider.GetDependency().Received(1) - .RaiseEventAsync(Arg.Is(referenceEvent => - referenceEvent.Type == ReferenceEventType.InvitedUsers && referenceEvent.Id == org.Id && - referenceEvent.Users == expectedNewUsersCount)); } [Theory] @@ -728,9 +717,8 @@ public class OrganizationServiceTests .UpdateSubscriptionAsync(Arg.Any()) .ReturnsForAnyArgs(Task.FromResult(0)).AndDoes(x => organization.SmSeats += invitedSmUsers); - // Throw error at the end of the try block - sutProvider.GetDependency().RaiseEventAsync(default) - .ThrowsForAnyArgs(); + sutProvider.GetDependency() + .SendInvitesAsync(Arg.Any()).ThrowsAsync(); sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) .Returns(StaticStore.GetPlan(organization.PlanType)); diff --git a/test/Core.Test/AdminConsole/Services/RabbitMqEventListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/RabbitMqEventListenerServiceTests.cs new file mode 100644 index 0000000000..8fd7e460be --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/RabbitMqEventListenerServiceTests.cs @@ -0,0 +1,173 @@ +using System.Text.Json; +using Bit.Core.Models.Data; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using Microsoft.Extensions.Logging; +using NSubstitute; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class RabbitMqEventListenerServiceTests +{ + private const string _queueName = "test_queue"; + private readonly IRabbitMqService _rabbitMqService = Substitute.For(); + private readonly ILogger _logger = Substitute.For>(); + + private SutProvider GetSutProvider() + { + return new SutProvider() + .SetDependency(_rabbitMqService) + .SetDependency(_logger) + .SetDependency(_queueName, "queueName") + .Create(); + } + + [Fact] + public async Task StartAsync_CreatesQueue() + { + var sutProvider = GetSutProvider(); + var cancellationToken = CancellationToken.None; + await sutProvider.Sut.StartAsync(cancellationToken); + + await _rabbitMqService.Received(1).CreateEventQueueAsync( + Arg.Is(_queueName), + Arg.Is(cancellationToken) + ); + } + + [Fact] + public async Task ProcessReceivedMessageAsync_EmptyJson_LogsError() + { + var sutProvider = GetSutProvider(); + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: new byte[0]); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + + [Fact] + public async Task ProcessReceivedMessageAsync_InvalidJson_LogsError() + { + var sutProvider = GetSutProvider(); + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: JsonSerializer.SerializeToUtf8Bytes("{ Inavlid JSON")); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Is(o => o.ToString().Contains("Invalid JSON")), + Arg.Any(), + Arg.Any>()); + } + + [Fact] + public async Task ProcessReceivedMessageAsync_InvalidJsonArray_LogsError() + { + var sutProvider = GetSutProvider(); + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: JsonSerializer.SerializeToUtf8Bytes(new[] { "not a valid", "list of event messages" })); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + + [Fact] + public async Task ProcessReceivedMessageAsync_InvalidJsonObject_LogsError() + { + var sutProvider = GetSutProvider(); + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: JsonSerializer.SerializeToUtf8Bytes(DateTime.UtcNow)); // wrong object - not EventMessage + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + + [Theory, BitAutoData] + public async Task ProcessReceivedMessageAsync_SingleEvent_DelegatesToHandler(EventMessage message) + { + var sutProvider = GetSutProvider(); + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: JsonSerializer.SerializeToUtf8Bytes(message)); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); + + await sutProvider.GetDependency().Received(1).HandleEventAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(message, new[] { "IdempotencyId" }))); + } + + [Theory, BitAutoData] + public async Task ProcessReceivedMessageAsync_ManyEvents_DelegatesToHandler(IEnumerable messages) + { + var sutProvider = GetSutProvider(); + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: JsonSerializer.SerializeToUtf8Bytes(messages)); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); + + await sutProvider.GetDependency().Received(1).HandleManyEventsAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(messages, new[] { "IdempotencyId" }))); + } +} diff --git a/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs new file mode 100644 index 0000000000..92a51e1831 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs @@ -0,0 +1,230 @@ +using System.Text; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using NSubstitute; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class RabbitMqIntegrationListenerServiceTests +{ + private const int _maxRetries = 3; + private const string _queueName = "test_queue"; + private const string _retryQueueName = "test_queue_retry"; + private const string _routingKey = "test_routing_key"; + private readonly IIntegrationHandler _handler = Substitute.For(); + private readonly IRabbitMqService _rabbitMqService = Substitute.For(); + + private SutProvider GetSutProvider() + { + return new SutProvider() + .SetDependency(_handler) + .SetDependency(_rabbitMqService) + .SetDependency(_queueName, "queueName") + .SetDependency(_retryQueueName, "retryQueueName") + .SetDependency(_routingKey, "routingKey") + .SetDependency(_maxRetries, "maxRetries") + .Create(); + } + + [Fact] + public async Task StartAsync_CreatesQueues() + { + var sutProvider = GetSutProvider(); + var cancellationToken = CancellationToken.None; + await sutProvider.Sut.StartAsync(cancellationToken); + + await _rabbitMqService.Received(1).CreateIntegrationQueuesAsync( + Arg.Is(_queueName), + Arg.Is(_retryQueueName), + Arg.Is(_routingKey), + Arg.Is(cancellationToken) + ); + } + + [Theory, BitAutoData] + public async Task ProcessReceivedMessageAsync_FailureNotRetryable_PublishesToDeadLetterQueue(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + var cancellationToken = CancellationToken.None; + await sutProvider.Sut.StartAsync(cancellationToken); + + message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + message.RetryCount = 0; + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: Encoding.UTF8.GetBytes(message.ToJson()) + ); + var result = new IntegrationHandlerResult(false, message); + result.Retryable = false; + _handler.HandleAsync(Arg.Any()).Returns(result); + + var expected = IntegrationMessage.FromJson(message.ToJson()); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken); + + await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); + + await _rabbitMqService.Received(1).PublishToDeadLetterAsync( + Arg.Any(), + Arg.Is(AssertHelper.AssertPropertyEqual(expected, new[] { "DelayUntilDate" })), + Arg.Any()); + + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .RepublishToRetryQueueAsync(default, default); + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .PublishToRetryAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task ProcessReceivedMessageAsync_FailureRetryableButTooManyRetries_PublishesToDeadLetterQueue(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + var cancellationToken = CancellationToken.None; + await sutProvider.Sut.StartAsync(cancellationToken); + + message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + message.RetryCount = _maxRetries; + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: Encoding.UTF8.GetBytes(message.ToJson()) + ); + var result = new IntegrationHandlerResult(false, message); + result.Retryable = true; + _handler.HandleAsync(Arg.Any()).Returns(result); + + var expected = IntegrationMessage.FromJson(message.ToJson()); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken); + + expected.ApplyRetry(result.DelayUntilDate); + await _rabbitMqService.Received(1).PublishToDeadLetterAsync( + Arg.Any(), + Arg.Is(AssertHelper.AssertPropertyEqual(expected, new[] { "DelayUntilDate" })), + Arg.Any()); + + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .RepublishToRetryQueueAsync(default, default); + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .PublishToRetryAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task ProcessReceivedMessageAsync_FailureRetryable_PublishesToRetryQueue(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + var cancellationToken = CancellationToken.None; + await sutProvider.Sut.StartAsync(cancellationToken); + + message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + message.RetryCount = 0; + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: Encoding.UTF8.GetBytes(message.ToJson()) + ); + var result = new IntegrationHandlerResult(false, message); + result.Retryable = true; + result.DelayUntilDate = DateTime.UtcNow.AddMinutes(1); + _handler.HandleAsync(Arg.Any()).Returns(result); + + var expected = IntegrationMessage.FromJson(message.ToJson()); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken); + + await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); + + expected.ApplyRetry(result.DelayUntilDate); + await _rabbitMqService.Received(1).PublishToRetryAsync( + Arg.Any(), + Arg.Is(AssertHelper.AssertPropertyEqual(expected, new[] { "DelayUntilDate" })), + Arg.Any()); + + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .RepublishToRetryQueueAsync(default, default); + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .PublishToDeadLetterAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task ProcessReceivedMessageAsync_SuccessfulResult_Succeeds(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + var cancellationToken = CancellationToken.None; + await sutProvider.Sut.StartAsync(cancellationToken); + + message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: Encoding.UTF8.GetBytes(message.ToJson()) + ); + var result = new IntegrationHandlerResult(true, message); + _handler.HandleAsync(Arg.Any()).Returns(result); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken); + + await _handler.Received(1).HandleAsync(Arg.Is(message.ToJson())); + + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .RepublishToRetryQueueAsync(default, default); + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .PublishToRetryAsync(default, default, default); + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .PublishToDeadLetterAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task ProcessReceivedMessageAsync_TooEarlyRetry_RepublishesToRetryQueue(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + var cancellationToken = CancellationToken.None; + await sutProvider.Sut.StartAsync(cancellationToken); + + message.DelayUntilDate = DateTime.UtcNow.AddMinutes(1); + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: Encoding.UTF8.GetBytes(message.ToJson()) + ); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken); + + await _rabbitMqService.Received(1) + .RepublishToRetryQueueAsync(Arg.Any(), Arg.Any()); + + await _handler.DidNotReceiveWithAnyArgs().HandleAsync(default); + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .PublishToRetryAsync(default, default, default); + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .PublishToDeadLetterAsync(default, default, default); + } +} diff --git a/test/Core.Test/AdminConsole/Services/SlackServiceTests.cs b/test/Core.Test/AdminConsole/Services/SlackServiceTests.cs index 93c0aa8dd4..92544551e0 100644 --- a/test/Core.Test/AdminConsole/Services/SlackServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/SlackServiceTests.cs @@ -1,4 +1,6 @@ -using System.Net; +#nullable enable + +using System.Net; using System.Text.Json; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; @@ -257,10 +259,10 @@ public class SlackServiceTests public void GetRedirectUrl_ReturnsCorrectUrl() { var sutProvider = GetSutProvider(); - var ClientId = sutProvider.GetDependency().Slack.ClientId; - var Scopes = sutProvider.GetDependency().Slack.Scopes; + var clientId = sutProvider.GetDependency().Slack.ClientId; + var scopes = sutProvider.GetDependency().Slack.Scopes; var redirectUrl = "https://example.com/callback"; - var expectedUrl = $"https://slack.com/oauth/v2/authorize?client_id={ClientId}&scope={Scopes}&redirect_uri={redirectUrl}"; + var expectedUrl = $"https://slack.com/oauth/v2/authorize?client_id={clientId}&scope={scopes}&redirect_uri={redirectUrl}"; var result = sutProvider.Sut.GetRedirectUrl(redirectUrl); Assert.Equal(expectedUrl, result); } diff --git a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs index 79c7569ea3..7870f543d1 100644 --- a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs @@ -79,6 +79,7 @@ public class WebhookIntegrationHandlerTests Assert.Equal(result.Message, message); Assert.True(result.DelayUntilDate.HasValue); Assert.InRange(result.DelayUntilDate.Value, DateTime.UtcNow.AddSeconds(59), DateTime.UtcNow.AddSeconds(61)); + Assert.Equal("Too Many Requests", result.FailureReason); } [Theory, BitAutoData] @@ -99,6 +100,7 @@ public class WebhookIntegrationHandlerTests Assert.Equal(result.Message, message); Assert.True(result.DelayUntilDate.HasValue); Assert.InRange(result.DelayUntilDate.Value, DateTime.UtcNow.AddSeconds(59), DateTime.UtcNow.AddSeconds(61)); + Assert.Equal("Too Many Requests", result.FailureReason); } [Theory, BitAutoData] @@ -117,6 +119,7 @@ public class WebhookIntegrationHandlerTests Assert.True(result.Retryable); Assert.Equal(result.Message, message); Assert.False(result.DelayUntilDate.HasValue); + Assert.Equal("Internal Server Error", result.FailureReason); } [Theory, BitAutoData] @@ -135,5 +138,6 @@ public class WebhookIntegrationHandlerTests Assert.False(result.Retryable); Assert.Equal(result.Message, message); Assert.Null(result.DelayUntilDate); + Assert.Equal("Temporary Redirect", result.FailureReason); } } diff --git a/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs b/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs index ffc56e89b2..b19ae47cfc 100644 --- a/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs +++ b/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs @@ -14,9 +14,6 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tokens; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -57,10 +54,6 @@ public class RegisterUserCommandTests await sutProvider.GetDependency() .Received(1) .SendWelcomeEmailAsync(user); - - await sutProvider.GetDependency() - .Received(1) - .RaiseEventAsync(Arg.Is(refEvent => refEvent.Type == ReferenceEventType.Signup)); } [Theory] @@ -85,10 +78,6 @@ public class RegisterUserCommandTests await sutProvider.GetDependency() .DidNotReceive() .SendWelcomeEmailAsync(Arg.Any()); - - await sutProvider.GetDependency() - .DidNotReceive() - .RaiseEventAsync(Arg.Any()); } // ----------------------------------------------------------------------------------------------- @@ -117,10 +106,6 @@ public class RegisterUserCommandTests await sutProvider.GetDependency() .Received(1) .CreateUserAsync(user, masterPasswordHash); - - await sutProvider.GetDependency() - .Received(1) - .RaiseEventAsync(Arg.Is(refEvent => refEvent.Type == ReferenceEventType.Signup)); } // Complex happy path test @@ -215,18 +200,9 @@ public class RegisterUserCommandTests .Received(1) .SendWelcomeEmailAsync(user); } - - await sutProvider.GetDependency() - .Received(1) - .RaiseEventAsync(Arg.Is(refEvent => refEvent.Type == ReferenceEventType.Signup && refEvent.SignupInitiationPath == initiationPath)); - } else { - await sutProvider.GetDependency() - .Received(1) - .RaiseEventAsync(Arg.Is(refEvent => refEvent.Type == ReferenceEventType.Signup && refEvent.SignupInitiationPath == default)); - // Even if user doesn't have reference data, we should send them welcome email await sutProvider.GetDependency() .Received(1) @@ -359,10 +335,6 @@ public class RegisterUserCommandTests await sutProvider.GetDependency() .Received(1) .SendWelcomeEmailAsync(user); - - await sutProvider.GetDependency() - .Received(1) - .RaiseEventAsync(Arg.Is(refEvent => refEvent.Type == ReferenceEventType.Signup && refEvent.ReceiveMarketingEmails == receiveMarketingMaterials)); } [Theory] @@ -429,10 +401,6 @@ public class RegisterUserCommandTests await sutProvider.GetDependency() .Received(1) .SendWelcomeEmailAsync(user); - - await sutProvider.GetDependency() - .Received(1) - .RaiseEventAsync(Arg.Is(refEvent => refEvent.Type == ReferenceEventType.Signup)); } [Theory] @@ -506,10 +474,6 @@ public class RegisterUserCommandTests await sutProvider.GetDependency() .Received(1) .SendWelcomeEmailAsync(user); - - await sutProvider.GetDependency() - .Received(1) - .RaiseEventAsync(Arg.Is(refEvent => refEvent.Type == ReferenceEventType.Signup)); } [Theory] @@ -604,10 +568,6 @@ public class RegisterUserCommandTests await sutProvider.GetDependency() .Received(1) .SendWelcomeEmailAsync(user); - - await sutProvider.GetDependency() - .Received(1) - .RaiseEventAsync(Arg.Is(refEvent => refEvent.Type == ReferenceEventType.Signup)); } [Theory] diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index a5bfe35152..9252d28588 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -26,7 +26,6 @@ using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Bit.Core.Vault.Repositories; using Bit.Test.Common.AutoFixture; @@ -316,7 +315,6 @@ public class UserServiceTests sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), - sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), @@ -910,7 +908,6 @@ public class UserServiceTests sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), - sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), diff --git a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs index f73a628940..5605b5ab2a 100644 --- a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs +++ b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs @@ -9,10 +9,7 @@ using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Test.AutoFixture.CipherFixtures; -using Bit.Core.Tools.Enums; using Bit.Core.Tools.ImportFeatures; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; @@ -183,8 +180,6 @@ public class ImportCiphersAsyncCommandTests !cus.Any(cu => cu.CollectionId == collections[0].Id) && // Check that access was not added for the collection that already existed in the organization cus.All(cu => cu.OrganizationUserId == importingOrganizationUser.Id && cu.Manage == true))); await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); - await sutProvider.GetDependency().Received(1).RaiseEventAsync( - Arg.Is(e => e.Type == ReferenceEventType.VaultImported)); } [Theory, BitAutoData] diff --git a/test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs b/test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs index 15e7d57651..674cca7d5f 100644 --- a/test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs +++ b/test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs @@ -8,7 +8,6 @@ using Bit.Core.Test.AutoFixture.CurrentContextFixtures; using Bit.Core.Test.Tools.AutoFixture.SendFixtures; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Repositories; using Bit.Core.Tools.SendFeatures; @@ -32,7 +31,6 @@ public class NonAnonymousSendCommandTests private readonly ISendAuthorizationService _sendAuthorizationService; private readonly ISendValidationService _sendValidationService; private readonly IFeatureService _featureService; - private readonly IReferenceEventService _referenceEventService; private readonly ICurrentContext _currentContext; private readonly ISendCoreHelperService _sendCoreHelperService; private readonly NonAnonymousSendCommand _nonAnonymousSendCommand; @@ -45,7 +43,6 @@ public class NonAnonymousSendCommandTests _sendAuthorizationService = Substitute.For(); _featureService = Substitute.For(); _sendValidationService = Substitute.For(); - _referenceEventService = Substitute.For(); _currentContext = Substitute.For(); _sendCoreHelperService = Substitute.For(); @@ -55,8 +52,6 @@ public class NonAnonymousSendCommandTests _pushNotificationService, _sendAuthorizationService, _sendValidationService, - _referenceEventService, - _currentContext, _sendCoreHelperService ); } @@ -135,14 +130,6 @@ public class NonAnonymousSendCommandTests // For new Sends await _sendRepository.Received(1).CreateAsync(send); await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send); - await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => - e.Id == userId && - e.Type == ReferenceEventType.SendCreated && - e.Source == ReferenceEventSource.User && - e.SendType == send.Type && - e.SendHasNotes == true && - e.ClientId == "test-client" && - e.ClientVersion == Version.Parse("1.0.0"))); } else { @@ -150,7 +137,6 @@ public class NonAnonymousSendCommandTests await _sendRepository.Received(1).UpsertAsync(send); Assert.NotEqual(initialDate, send.RevisionDate); await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); - await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); } } @@ -234,14 +220,6 @@ public class NonAnonymousSendCommandTests // For new Sends await _sendRepository.Received(1).CreateAsync(send); await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send); - await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => - e.Id == userId && - e.Type == ReferenceEventType.SendCreated && - e.Source == ReferenceEventSource.User && - e.SendType == send.Type && - e.HasPassword == false && - e.ClientId == "test-client" && - e.ClientVersion == Version.Parse("1.0.0"))); } else { @@ -249,7 +227,6 @@ public class NonAnonymousSendCommandTests await _sendRepository.Received(1).UpsertAsync(send); Assert.NotEqual(initialDate, send.RevisionDate); await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); - await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); } } @@ -285,7 +262,6 @@ public class NonAnonymousSendCommandTests await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); - await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); } [Theory] @@ -328,14 +304,6 @@ public class NonAnonymousSendCommandTests // For new Sends await _sendRepository.Received(1).CreateAsync(send); await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send); - await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => - e.Id == userId && - e.Type == ReferenceEventType.SendCreated && - e.Source == ReferenceEventSource.User && - e.SendType == send.Type && - e.SendHasNotes == true && - e.ClientId == "test-client" && - e.ClientVersion == Version.Parse("1.0.0"))); } else { @@ -343,7 +311,6 @@ public class NonAnonymousSendCommandTests await _sendRepository.Received(1).UpsertAsync(send); Assert.NotEqual(initialDate, send.RevisionDate); await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); - await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); } } @@ -386,9 +353,6 @@ public class NonAnonymousSendCommandTests // Verify push notification wasn't sent await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); - - // Verify reference event service wasn't called - await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); } [Theory] @@ -431,13 +395,6 @@ public class NonAnonymousSendCommandTests // For new Sends await _sendRepository.Received(1).CreateAsync(send); await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send); - await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => - e.Id == userId && - e.Type == ReferenceEventType.SendCreated && - e.Source == ReferenceEventSource.User && - e.SendType == send.Type && - e.ClientId == "test-client" && - e.ClientVersion == Version.Parse("1.0.0"))); } else { @@ -445,7 +402,6 @@ public class NonAnonymousSendCommandTests await _sendRepository.Received(1).UpsertAsync(send); Assert.NotEqual(initialDate, send.RevisionDate); await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); - await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); } } @@ -481,9 +437,6 @@ public class NonAnonymousSendCommandTests // Verify push notification was sent for the update await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); - - // Verify no reference event was raised (only happens for new sends) - await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); } [Fact] diff --git a/test/Core.Test/Tools/Services/SendAuthorizationServiceTests.cs b/test/Core.Test/Tools/Services/SendAuthorizationServiceTests.cs index 9b2637d030..c33dbc0ec6 100644 --- a/test/Core.Test/Tools/Services/SendAuthorizationServiceTests.cs +++ b/test/Core.Test/Tools/Services/SendAuthorizationServiceTests.cs @@ -1,5 +1,4 @@ -using Bit.Core.Context; -using Bit.Core.Platform.Push; +using Bit.Core.Platform.Push; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Repositories; @@ -15,8 +14,6 @@ public class SendAuthorizationServiceTests private readonly ISendRepository _sendRepository; private readonly IPasswordHasher _passwordHasher; private readonly IPushNotificationService _pushNotificationService; - private readonly IReferenceEventService _referenceEventService; - private readonly ICurrentContext _currentContext; private readonly SendAuthorizationService _sendAuthorizationService; public SendAuthorizationServiceTests() @@ -24,15 +21,11 @@ public class SendAuthorizationServiceTests _sendRepository = Substitute.For(); _passwordHasher = Substitute.For>(); _pushNotificationService = Substitute.For(); - _referenceEventService = Substitute.For(); - _currentContext = Substitute.For(); _sendAuthorizationService = new SendAuthorizationService( _sendRepository, _passwordHasher, - _pushNotificationService, - _referenceEventService, - _currentContext); + _pushNotificationService); } diff --git a/test/Core.Test/Vault/Services/CipherServiceTests.cs b/test/Core.Test/Vault/Services/CipherServiceTests.cs index 061d90bcc3..95fd8179e3 100644 --- a/test/Core.Test/Vault/Services/CipherServiceTests.cs +++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs @@ -72,7 +72,7 @@ public class CipherServiceTests [Theory, BitAutoData] public async Task ShareManyAsync_WrongRevisionDate_Throws(SutProvider sutProvider, - IEnumerable ciphers, Guid organizationId, List collectionIds) + IEnumerable ciphers, Guid organizationId, List collectionIds) { sutProvider.GetDependency().GetByIdAsync(organizationId) .Returns(new Organization @@ -651,7 +651,7 @@ public class CipherServiceTests [BitAutoData("")] [BitAutoData("Correct Time")] public async Task ShareManyAsync_CorrectRevisionDate_Passes(string revisionDateString, - SutProvider sutProvider, IEnumerable ciphers, Organization organization, List collectionIds) + SutProvider sutProvider, IEnumerable ciphers, Organization organization, List collectionIds) { sutProvider.GetDependency().GetByIdAsync(organization.Id) .Returns(new Organization @@ -1173,7 +1173,7 @@ public class CipherServiceTests [Theory, BitAutoData] public async Task ShareManyAsync_FreeOrgWithAttachment_Throws(SutProvider sutProvider, - IEnumerable ciphers, Guid organizationId, List collectionIds) + IEnumerable ciphers, Guid organizationId, List collectionIds) { sutProvider.GetDependency().GetByIdAsync(organizationId).Returns(new Organization { @@ -1194,7 +1194,7 @@ public class CipherServiceTests [Theory, BitAutoData] public async Task ShareManyAsync_PaidOrgWithAttachment_Passes(SutProvider sutProvider, - IEnumerable ciphers, Guid organizationId, List collectionIds) + IEnumerable ciphers, Guid organizationId, List collectionIds) { sutProvider.GetDependency().GetByIdAsync(organizationId) .Returns(new Organization diff --git a/test/Events.IntegrationTest/EventsApplicationFactory.cs b/test/Events.IntegrationTest/EventsApplicationFactory.cs index b1c3ef8bf5..7d692c442a 100644 --- a/test/Events.IntegrationTest/EventsApplicationFactory.cs +++ b/test/Events.IntegrationTest/EventsApplicationFactory.cs @@ -1,11 +1,11 @@ using Bit.Core; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Enums; +using Bit.IntegrationTestCommon; using Bit.IntegrationTestCommon.Factories; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; -using Microsoft.Data.Sqlite; using Microsoft.Extensions.DependencyInjection; namespace Bit.Events.IntegrationTest; @@ -13,15 +13,18 @@ namespace Bit.Events.IntegrationTest; public class EventsApplicationFactory : WebApplicationFactoryBase { private readonly IdentityApplicationFactory _identityApplicationFactory; - private const string _connectionString = "DataSource=:memory:"; - public EventsApplicationFactory() + public EventsApplicationFactory() : this(new SqliteTestDatabase()) { - SqliteConnection = new SqliteConnection(_connectionString); - SqliteConnection.Open(); + } + + protected EventsApplicationFactory(ITestDatabase db) + { + TestDatabase = db; _identityApplicationFactory = new IdentityApplicationFactory(); - _identityApplicationFactory.SqliteConnection = SqliteConnection; + _identityApplicationFactory.TestDatabase = TestDatabase; + _identityApplicationFactory.ManagesDatabase = false; } protected override void ConfigureWebHost(IWebHostBuilder builder) @@ -42,6 +45,10 @@ public class EventsApplicationFactory : WebApplicationFactoryBase /// public async Task<(string Token, string RefreshToken)> LoginWithNewAccount(string email = "integration-test@bitwarden.com", string masterPasswordHash = "master_password_hash") { + // This might be the first action in a test and since it forwards to the Identity server, we need to ensure that + // this server is initialized since it's responsible for seeding the database. + Assert.NotNull(Services); + await _identityApplicationFactory.RegisterNewIdentityFactoryUserAsync( new RegisterFinishRequestModel { @@ -59,10 +66,4 @@ public class EventsApplicationFactory : WebApplicationFactoryBase return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash); } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - SqliteConnection!.Dispose(); - } } diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs index 80f2b5e20b..e63858117f 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs @@ -238,7 +238,7 @@ public class IdentityServerTests : IClassFixture } [Theory, BitAutoData, RegisterFinishRequestModelCustomize] - public async Task TokenEndpoint_GrantTypeClientCredentials_AsLegacyUser_NotOnWebClient_Fails( + public async Task TokenEndpoint_GrantTypeClientCredentials_AsLegacyUser_Fails( RegisterFinishRequestModel model, string deviceId) { @@ -277,7 +277,7 @@ public class IdentityServerTests : IClassFixture var errorBody = await AssertHelper.AssertResponseTypeIs(context); var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "ErrorModel", JsonValueKind.Object); var message = AssertHelper.AssertJsonProperty(error, "Message", JsonValueKind.String).GetString(); - Assert.StartsWith("Encryption key migration is required.", message); + Assert.StartsWith("Legacy encryption without a userkey is no longer supported.", message); } diff --git a/test/Identity.Test/Controllers/AccountsControllerTests.cs b/test/Identity.Test/Controllers/AccountsControllerTests.cs index a045490862..bff33cc679 100644 --- a/test/Identity.Test/Controllers/AccountsControllerTests.cs +++ b/test/Identity.Test/Controllers/AccountsControllerTests.cs @@ -14,9 +14,6 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tokens; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Identity.Controllers; using Bit.Identity.Models.Request.Accounts; using Bit.Test.Common.AutoFixture.Attributes; @@ -40,7 +37,6 @@ public class AccountsControllerTests : IDisposable private readonly IDataProtectorTokenFactory _assertionOptionsDataProtector; private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand; private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand; - private readonly IReferenceEventService _referenceEventService; private readonly IFeatureService _featureService; private readonly IDataProtectorTokenFactory _registrationEmailVerificationTokenDataFactory; private readonly GlobalSettings _globalSettings; @@ -55,7 +51,6 @@ public class AccountsControllerTests : IDisposable _assertionOptionsDataProtector = Substitute.For>(); _getWebAuthnLoginCredentialAssertionOptionsCommand = Substitute.For(); _sendVerificationEmailForRegistrationCommand = Substitute.For(); - _referenceEventService = Substitute.For(); _featureService = Substitute.For(); _registrationEmailVerificationTokenDataFactory = Substitute.For>(); _globalSettings = Substitute.For(); @@ -68,7 +63,6 @@ public class AccountsControllerTests : IDisposable _assertionOptionsDataProtector, _getWebAuthnLoginCredentialAssertionOptionsCommand, _sendVerificationEmailForRegistrationCommand, - _referenceEventService, _featureService, _registrationEmailVerificationTokenDataFactory, _globalSettings @@ -163,8 +157,6 @@ public class AccountsControllerTests : IDisposable var okResult = Assert.IsType(result); Assert.Equal(200, okResult.StatusCode); Assert.Equal(token, okResult.Value); - - await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => e.Type == ReferenceEventType.SignupEmailSubmit)); } [Theory] @@ -187,7 +179,6 @@ public class AccountsControllerTests : IDisposable // Assert var noContentResult = Assert.IsType(result); Assert.Equal(204, noContentResult.StatusCode); - await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => e.Type == ReferenceEventType.SignupEmailSubmit)); } [Theory, BitAutoData] @@ -404,12 +395,6 @@ public class AccountsControllerTests : IDisposable // Assert var okResult = Assert.IsType(result); Assert.Equal(200, okResult.StatusCode); - - await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => - e.Type == ReferenceEventType.SignupEmailClicked - && e.EmailVerificationTokenValid == true - && e.UserAlreadyExists == false - )); } [Theory, BitAutoData] @@ -435,12 +420,6 @@ public class AccountsControllerTests : IDisposable // Act & assert await Assert.ThrowsAsync(() => _sut.PostRegisterVerificationEmailClicked(requestModel)); - - await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => - e.Type == ReferenceEventType.SignupEmailClicked - && e.EmailVerificationTokenValid == false - && e.UserAlreadyExists == false - )); } @@ -467,12 +446,6 @@ public class AccountsControllerTests : IDisposable // Act & assert await Assert.ThrowsAsync(() => _sut.PostRegisterVerificationEmailClicked(requestModel)); - - await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => - e.Type == ReferenceEventType.SignupEmailClicked - && e.EmailVerificationTokenValid == true - && e.UserAlreadyExists == true - )); } private void SetDefaultKdfHmacKey(byte[]? newKey) diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index 9eb17da88a..aab98a583c 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -373,8 +373,7 @@ public class BaseRequestValidatorTests // Assert Assert.True(context.GrantResult.IsError); var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"]; - var expectedMessage = $"Encryption key migration is required. Please log in to the web " + - $"vault at {_globalSettings.BaseServiceUri.VaultWithHash}"; + var expectedMessage = "Legacy encryption without a userkey is no longer supported. To recover your account, please contact support"; Assert.Equal(expectedMessage, errorResponse.Message); } diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs index 76fa0f03d1..e406915e38 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs @@ -4,12 +4,10 @@ using Bit.Core.Platform.Push; using Bit.Core.Platform.Push.Internal; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Tools.Services; using Bit.Infrastructure.EntityFramework.Repositories; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; -using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -37,14 +35,19 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory /// /// This will need to be set BEFORE using the Server property /// - public SqliteConnection? SqliteConnection { get; set; } + public ITestDatabase TestDatabase { get; set; } = new SqliteTestDatabase(); + + /// + /// If set to true the factory will manage the database lifecycle, including migrations. + /// + /// + /// This will need to be set BEFORE using the Server property + /// + public bool ManagesDatabase { get; set; } = true; private readonly List> _configureTestServices = new(); private readonly List> _configureAppConfiguration = new(); - private bool _handleSqliteDisposal { get; set; } - - public void SubstituteService(Action mockService) where TService : class { @@ -119,12 +122,41 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory /// protected override void ConfigureWebHost(IWebHostBuilder builder) { - if (SqliteConnection == null) + var config = new Dictionary { - SqliteConnection = new SqliteConnection("DataSource=:memory:"); - SqliteConnection.Open(); - _handleSqliteDisposal = true; - } + // Manually insert a EF provider so that ConfigureServices will add EF repositories but we will override + // DbContextOptions to use an in memory database + { "globalSettings:databaseProvider", "postgres" }, + { "globalSettings:postgreSql:connectionString", "Host=localhost;Username=test;Password=test;Database=test" }, + + // Clear the redis connection string for distributed caching, forcing an in-memory implementation + { "globalSettings:redis:connectionString", "" }, + + // Clear Storage + { "globalSettings:attachment:connectionString", null }, + { "globalSettings:events:connectionString", null }, + { "globalSettings:send:connectionString", null }, + { "globalSettings:notifications:connectionString", null }, + { "globalSettings:storage:connectionString", null }, + + // This will force it to use an ephemeral key for IdentityServer + { "globalSettings:developmentDirectory", null }, + + // Email Verification + { "globalSettings:enableEmailVerification", "true" }, + { "globalSettings:disableUserRegistration", "false" }, + { "globalSettings:launchDarkly:flagValues:email-verification", "true" }, + + // New Device Verification + { "globalSettings:disableEmailNewDevice", "false" }, + + // Web push notifications + { "globalSettings:webPush:vapidPublicKey", "BGBtAM0bU3b5jsB14IjBYarvJZ6rWHilASLudTTYDDBi7a-3kebo24Yus_xYeOMZ863flAXhFAbkL6GVSrxgErg" }, + { "globalSettings:launchDarkly:flagValues:web-push", "true" }, + }; + + // Some database drivers modify the connection string + TestDatabase.ModifyGlobalSettings(config); builder.ConfigureAppConfiguration(c => { @@ -134,39 +166,7 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory c.AddUserSecrets(typeof(Identity.Startup).Assembly, optional: true); - c.AddInMemoryCollection(new Dictionary - { - // Manually insert a EF provider so that ConfigureServices will add EF repositories but we will override - // DbContextOptions to use an in memory database - { "globalSettings:databaseProvider", "postgres" }, - { "globalSettings:postgreSql:connectionString", "Host=localhost;Username=test;Password=test;Database=test" }, - - // Clear the redis connection string for distributed caching, forcing an in-memory implementation - { "globalSettings:redis:connectionString", ""}, - - // Clear Storage - { "globalSettings:attachment:connectionString", null}, - { "globalSettings:events:connectionString", null}, - { "globalSettings:send:connectionString", null}, - { "globalSettings:notifications:connectionString", null}, - { "globalSettings:storage:connectionString", null}, - - // This will force it to use an ephemeral key for IdentityServer - { "globalSettings:developmentDirectory", null }, - - - // Email Verification - { "globalSettings:enableEmailVerification", "true" }, - { "globalSettings:disableUserRegistration", "false" }, - { "globalSettings:launchDarkly:flagValues:email-verification", "true" }, - - // New Device Verification - { "globalSettings:disableEmailNewDevice", "false" }, - - // Web push notifications - { "globalSettings:webPush:vapidPublicKey", "BGBtAM0bU3b5jsB14IjBYarvJZ6rWHilASLudTTYDDBi7a-3kebo24Yus_xYeOMZ863flAXhFAbkL6GVSrxgErg" }, - { "globalSettings:launchDarkly:flagValues:web-push", "true" }, - }); + c.AddInMemoryCollection(config); }); // Run configured actions after defaults to allow them to take precedence @@ -177,17 +177,16 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory builder.ConfigureTestServices(services => { - var dbContextOptions = services.First(sd => sd.ServiceType == typeof(DbContextOptions)); + var dbContextOptions = + services.First(sd => sd.ServiceType == typeof(DbContextOptions)); services.Remove(dbContextOptions); - services.AddScoped(services => - { - return new DbContextOptionsBuilder() - .UseSqlite(SqliteConnection) - .UseApplicationServiceProvider(services) - .Options; - }); - MigrateDbContext(services); + // Add database to the service collection + TestDatabase.AddDatabase(services); + if (ManagesDatabase) + { + TestDatabase.Migrate(services); + } // QUESTION: The normal licensing service should run fine on developer machines but not in CI // should we have a fork here to leave the normal service for developers? @@ -209,9 +208,6 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory // TODO: Install and use azurite in CI pipeline Replace(services); - // TODO: Install and use azurite in CI pipeline - Replace(services); - // Our Rate limiter works so well that it begins to fail tests unless we carve out // one whitelisted ip. We should still test the rate limiter though and they should change the Ip // to something that is NOT whitelisted @@ -286,22 +282,11 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory protected override void Dispose(bool disposing) { base.Dispose(disposing); - if (_handleSqliteDisposal) + if (ManagesDatabase) { - SqliteConnection!.Dispose(); + // Avoid calling Dispose twice + ManagesDatabase = false; + TestDatabase.Dispose(); } } - - private void MigrateDbContext(IServiceCollection serviceCollection) where TContext : DbContext - { - var serviceProvider = serviceCollection.BuildServiceProvider(); - using var scope = serviceProvider.CreateScope(); - var services = scope.ServiceProvider; - var context = services.GetRequiredService(); - if (_handleSqliteDisposal) - { - context.Database.EnsureDeleted(); - } - context.Database.EnsureCreated(); - } } diff --git a/test/IntegrationTestCommon/ITestDatabase.cs b/test/IntegrationTestCommon/ITestDatabase.cs new file mode 100644 index 0000000000..c6d51e428f --- /dev/null +++ b/test/IntegrationTestCommon/ITestDatabase.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.IntegrationTestCommon; + +#nullable enable + +public interface ITestDatabase +{ + public void AddDatabase(IServiceCollection serviceCollection); + + public void Migrate(IServiceCollection serviceCollection); + + public void Dispose(); + + public void ModifyGlobalSettings(Dictionary config) + { + // Default implementation does nothing + } +} diff --git a/test/IntegrationTestCommon/IntegrationTestCommon.csproj b/test/IntegrationTestCommon/IntegrationTestCommon.csproj index 3e8e55524b..a20a14f222 100644 --- a/test/IntegrationTestCommon/IntegrationTestCommon.csproj +++ b/test/IntegrationTestCommon/IntegrationTestCommon.csproj @@ -11,6 +11,7 @@ + diff --git a/test/IntegrationTestCommon/SqlServerTestDatabase.cs b/test/IntegrationTestCommon/SqlServerTestDatabase.cs new file mode 100644 index 0000000000..93ffa90d3d --- /dev/null +++ b/test/IntegrationTestCommon/SqlServerTestDatabase.cs @@ -0,0 +1,83 @@ +using Bit.Core.Settings; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Migrator; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Bit.IntegrationTestCommon; + +public class SqlServerTestDatabase : ITestDatabase +{ + private string _sqlServerConnection { get; set; } + + public SqlServerTestDatabase() + { + // Grab the connection string from the Identity project user secrets + var identityBuilder = new ConfigurationBuilder(); + identityBuilder.AddUserSecrets(typeof(Identity.Startup).Assembly, optional: true); + var identityConfig = identityBuilder.Build(); + var identityConnectionString = identityConfig.GetSection("globalSettings:sqlServer:connectionString").Value; + + // Replace the database name in the connection string to use a test database + var testConnectionString = new SqlConnectionStringBuilder(identityConnectionString) + { + InitialCatalog = "vault_test" + }.ConnectionString; + + _sqlServerConnection = testConnectionString; + } + + public void ModifyGlobalSettings(Dictionary config) + { + config["globalSettings:databaseProvider"] = "sqlserver"; + config["globalSettings:sqlServer:connectionString"] = _sqlServerConnection; + } + + public void AddDatabase(IServiceCollection serviceCollection) + { + serviceCollection.AddScoped(s => new DbContextOptionsBuilder() + .UseSqlServer(_sqlServerConnection) + .UseApplicationServiceProvider(s) + .Options); + } + + public void Migrate(IServiceCollection serviceCollection) + { + var serviceProvider = serviceCollection.BuildServiceProvider(); + using var scope = serviceProvider.CreateScope(); + var services = scope.ServiceProvider; + var globalSettings = services.GetRequiredService(); + var logger = services.GetRequiredService>(); + + var migrator = new SqlServerDbMigrator(globalSettings, logger); + migrator.MigrateDatabase(); + } + + public void Dispose() + { + var masterConnectionString = new SqlConnectionStringBuilder(_sqlServerConnection) + { + InitialCatalog = "master" + }.ConnectionString; + + using var connection = new SqlConnection(masterConnectionString); + var databaseName = new SqlConnectionStringBuilder(_sqlServerConnection).InitialCatalog; + + connection.Open(); + + var databaseNameQuoted = new SqlCommandBuilder().QuoteIdentifier(databaseName); + + using (var cmd = new SqlCommand($"ALTER DATABASE {databaseNameQuoted} SET single_user WITH rollback IMMEDIATE", connection)) + { + cmd.ExecuteNonQuery(); + } + + using (var cmd = new SqlCommand($"DROP DATABASE {databaseNameQuoted}", connection)) + { + cmd.ExecuteNonQuery(); + } + } +} diff --git a/test/IntegrationTestCommon/SqliteTestDatabase.cs b/test/IntegrationTestCommon/SqliteTestDatabase.cs new file mode 100644 index 0000000000..7d1ec2f07e --- /dev/null +++ b/test/IntegrationTestCommon/SqliteTestDatabase.cs @@ -0,0 +1,41 @@ +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.IntegrationTestCommon; + +public class SqliteTestDatabase : ITestDatabase +{ + private SqliteConnection SqliteConnection { get; set; } + + public SqliteTestDatabase() + { + SqliteConnection = new SqliteConnection("DataSource=:memory:"); + SqliteConnection.Open(); + } + + public void AddDatabase(IServiceCollection serviceCollection) + { + serviceCollection.AddScoped(s => new DbContextOptionsBuilder() + .UseSqlite(SqliteConnection) + .UseApplicationServiceProvider(s) + .Options); + } + + public void Migrate(IServiceCollection serviceCollection) + { + var serviceProvider = serviceCollection.BuildServiceProvider(); + using var scope = serviceProvider.CreateScope(); + var services = scope.ServiceProvider; + var context = services.GetRequiredService(); + + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + } + + public void Dispose() + { + SqliteConnection.Dispose(); + } +} diff --git a/util/Setup/Program.cs b/util/Setup/Program.cs index 50f3046d6d..921c32f5e6 100644 --- a/util/Setup/Program.cs +++ b/util/Setup/Program.cs @@ -20,30 +20,29 @@ public class Program ParseParameters(); - if (_context.Parameters.ContainsKey("q")) + if (_context.Parameters.TryGetValue("q", out var q)) { - _context.Quiet = _context.Parameters["q"] == "true" || _context.Parameters["q"] == "1"; + _context.Quiet = q == "true" || q == "1"; } - if (_context.Parameters.ContainsKey("os")) + if (_context.Parameters.TryGetValue("os", out var os)) { - _context.HostOS = _context.Parameters["os"]; + _context.HostOS = os; } - if (_context.Parameters.ContainsKey("corev")) + if (_context.Parameters.TryGetValue("corev", out var coreVersion)) { - _context.CoreVersion = _context.Parameters["corev"]; + _context.CoreVersion = coreVersion; } - if (_context.Parameters.ContainsKey("webv")) + if (_context.Parameters.TryGetValue("webv", out var webVersion)) { - _context.WebVersion = _context.Parameters["webv"]; + _context.WebVersion = webVersion; } - if (_context.Parameters.ContainsKey("keyconnectorv")) + if (_context.Parameters.TryGetValue("keyconnectorv", out var keyConnectorVersion)) { - _context.KeyConnectorVersion = _context.Parameters["keyconnectorv"]; + _context.KeyConnectorVersion = keyConnectorVersion; } - if (_context.Parameters.ContainsKey("stub")) + if (_context.Parameters.TryGetValue("stub", out var stub)) { - _context.Stub = _context.Parameters["stub"] == "true" || - _context.Parameters["stub"] == "1"; + _context.Stub = stub == "true" || stub == "1"; } Helpers.WriteLine(_context); @@ -68,18 +67,18 @@ public class Program private static void Install() { - if (_context.Parameters.ContainsKey("letsencrypt")) + if (_context.Parameters.TryGetValue("letsencrypt", out var sslManagedLetsEncrypt)) { _context.Config.SslManagedLetsEncrypt = - _context.Parameters["letsencrypt"].ToLowerInvariant() == "y"; + sslManagedLetsEncrypt.ToLowerInvariant() == "y"; } - if (_context.Parameters.ContainsKey("domain")) + if (_context.Parameters.TryGetValue("domain", out var domain)) { - _context.Install.Domain = _context.Parameters["domain"].ToLowerInvariant(); + _context.Install.Domain = domain.ToLowerInvariant(); } - if (_context.Parameters.ContainsKey("dbname")) + if (_context.Parameters.TryGetValue("dbname", out var database)) { - _context.Install.Database = _context.Parameters["dbname"]; + _context.Install.Database = database; } if (_context.Stub)