1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-07 03:30:32 -05:00

Merge branch 'main' of https://github.com/bitwarden/server into vault/pm-20041/mark-task-complete

This commit is contained in:
Nick Krantz 2025-06-04 09:45:40 -05:00
commit bde0aa3352
No known key found for this signature in database
GPG Key ID: FF670021ABCAB82E
149 changed files with 2219 additions and 2079 deletions

View File

@ -12,6 +12,9 @@ on:
workflow_call: workflow_call:
inputs: {} inputs: {}
permissions:
contents: read
env: env:
_AZ_REGISTRY: "bitwardenprod.azurecr.io" _AZ_REGISTRY: "bitwardenprod.azurecr.io"
_GITHUB_PR_REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }} _GITHUB_PR_REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }}
@ -237,18 +240,10 @@ jobs:
fi fi
echo "tags=$TAGS" >> $GITHUB_OUTPUT 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 - name: Build Docker image
id: build-artifacts id: build-artifacts
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0 uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0
with: with:
cache-from: type=registry,ref=${{ steps.cache-name.outputs.name }}
cache-to: type=registry,ref=${{ steps.cache-name.outputs.name}},mode=max
context: . context: .
file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile
platforms: | platforms: |
@ -605,6 +600,7 @@ jobs:
project: server project: server
pull_request_number: ${{ github.event.number }} pull_request_number: ${{ github.event.number }}
secrets: inherit secrets: inherit
permissions: read-all
check-failures: check-failures:
name: Check for failures name: Check for failures

View File

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Version>2025.5.2</Version> <Version>2025.6.0</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace> <RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>

View File

@ -287,11 +287,10 @@ public class ProviderService : IProviderService
foreach (var user in users) foreach (var user in users)
{ {
if (!keyedFilteredUsers.ContainsKey(user.Id)) if (!keyedFilteredUsers.TryGetValue(user.Id, out var providerUser))
{ {
continue; continue;
} }
var providerUser = keyedFilteredUsers[user.Id];
try try
{ {
if (providerUser.Status != ProviderUserStatusType.Accepted || providerUser.ProviderId != providerId) if (providerUser.Status != ProviderUserStatusType.Accepted || providerUser.ProviderId != providerId)

View File

@ -16,8 +16,8 @@ public class Program
{ {
var context = e.Properties["SourceContext"].ToString(); var context = e.Properties["SourceContext"].ToString();
if (e.Properties.ContainsKey("RequestPath") && if (e.Properties.TryGetValue("RequestPath", out var requestPath) &&
!string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) && !string.IsNullOrWhiteSpace(requestPath?.ToString()) &&
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer"))) (context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
{ {
return false; return false;

View File

@ -370,8 +370,8 @@ public class AccountController : Controller
// for the user identifier. // for the user identifier.
static bool nameIdIsNotTransient(Claim c) => c.Type == ClaimTypes.NameIdentifier static bool nameIdIsNotTransient(Claim c) => c.Type == ClaimTypes.NameIdentifier
&& (c.Properties == null && (c.Properties == null
|| !c.Properties.ContainsKey(SamlPropertyKeys.ClaimFormat) || !c.Properties.TryGetValue(SamlPropertyKeys.ClaimFormat, out var claimFormat)
|| c.Properties[SamlPropertyKeys.ClaimFormat] != SamlNameIdFormats.Transient); || claimFormat != SamlNameIdFormats.Transient);
// Try to determine the unique id of the external user (issued by the provider) // 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 // the most common claim type for that are the sub claim and the NameIdentifier

View File

@ -17,8 +17,8 @@ public class Program
logging.AddSerilog(hostingContext, (e, globalSettings) => logging.AddSerilog(hostingContext, (e, globalSettings) =>
{ {
var context = e.Properties["SourceContext"].ToString(); var context = e.Properties["SourceContext"].ToString();
if (e.Properties.ContainsKey("RequestPath") && if (e.Properties.TryGetValue("RequestPath", out var requestPath) &&
!string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) && !string.IsNullOrWhiteSpace(requestPath?.ToString()) &&
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer"))) (context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
{ {
return false; return false;

View File

@ -46,9 +46,9 @@ public static class OpenIdConnectOptionsExtensions
// Handle State if we've gotten that back // Handle State if we've gotten that back
var decodedState = options.StateDataFormat.Unprotect(state); 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 catch

View File

@ -99,7 +99,7 @@ services:
- idp - idp
rabbitmq: rabbitmq:
image: rabbitmq:management image: rabbitmq:4.1.0-management
container_name: rabbitmq container_name: rabbitmq
ports: ports:
- "5672:5672" - "5672:5672"
@ -108,7 +108,7 @@ services:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER} RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS} RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}
volumes: volumes:
- rabbitmq_data:/var/lib/rabbitmq_data - rabbitmq_data:/var/lib/rabbitmq
profiles: profiles:
- rabbitmq - rabbitmq

View File

@ -5,6 +5,6 @@
}, },
"msbuild-sdks": { "msbuild-sdks": {
"Microsoft.Build.Traversal": "4.1.0", "Microsoft.Build.Traversal": "4.1.0",
"Microsoft.Build.Sql": "0.1.9-preview" "Microsoft.Build.Sql": "1.0.0"
} }
} }

View File

@ -12,7 +12,6 @@ using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Providers.Services;
using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.OrganizationConnectionConfigs; using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
@ -20,9 +19,6 @@ using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; 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.Utilities;
using Bit.Core.Vault.Repositories; using Bit.Core.Vault.Repositories;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -45,12 +41,9 @@ public class OrganizationsController : Controller
private readonly IPaymentService _paymentService; private readonly IPaymentService _paymentService;
private readonly IApplicationCacheService _applicationCacheService; private readonly IApplicationCacheService _applicationCacheService;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IReferenceEventService _referenceEventService;
private readonly IUserService _userService;
private readonly IProviderRepository _providerRepository; private readonly IProviderRepository _providerRepository;
private readonly ILogger<OrganizationsController> _logger; private readonly ILogger<OrganizationsController> _logger;
private readonly IAccessControlService _accessControlService; private readonly IAccessControlService _accessControlService;
private readonly ICurrentContext _currentContext;
private readonly ISecretRepository _secretRepository; private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository; private readonly IProjectRepository _projectRepository;
private readonly IServiceAccountRepository _serviceAccountRepository; private readonly IServiceAccountRepository _serviceAccountRepository;
@ -73,12 +66,9 @@ public class OrganizationsController : Controller
IPaymentService paymentService, IPaymentService paymentService,
IApplicationCacheService applicationCacheService, IApplicationCacheService applicationCacheService,
GlobalSettings globalSettings, GlobalSettings globalSettings,
IReferenceEventService referenceEventService,
IUserService userService,
IProviderRepository providerRepository, IProviderRepository providerRepository,
ILogger<OrganizationsController> logger, ILogger<OrganizationsController> logger,
IAccessControlService accessControlService, IAccessControlService accessControlService,
ICurrentContext currentContext,
ISecretRepository secretRepository, ISecretRepository secretRepository,
IProjectRepository projectRepository, IProjectRepository projectRepository,
IServiceAccountRepository serviceAccountRepository, IServiceAccountRepository serviceAccountRepository,
@ -100,12 +90,9 @@ public class OrganizationsController : Controller
_paymentService = paymentService; _paymentService = paymentService;
_applicationCacheService = applicationCacheService; _applicationCacheService = applicationCacheService;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_referenceEventService = referenceEventService;
_userService = userService;
_providerRepository = providerRepository; _providerRepository = providerRepository;
_logger = logger; _logger = logger;
_accessControlService = accessControlService; _accessControlService = accessControlService;
_currentContext = currentContext;
_secretRepository = secretRepository; _secretRepository = secretRepository;
_projectRepository = projectRepository; _projectRepository = projectRepository;
_serviceAccountRepository = serviceAccountRepository; _serviceAccountRepository = serviceAccountRepository;
@ -272,11 +259,6 @@ public class OrganizationsController : Controller
await _organizationRepository.ReplaceAsync(organization); await _organizationRepository.ReplaceAsync(organization);
await _applicationCacheService.UpsertOrganizationAbilityAsync(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 }); return RedirectToAction("Edit", new { id });
} }

View File

@ -39,7 +39,7 @@ public class ReadOnlyEnvIdentityUserStore : ReadOnlyIdentityUserStore
} }
} }
var userStamp = usersDict.ContainsKey(normalizedEmail) ? usersDict[normalizedEmail] : null; var userStamp = usersDict.GetValueOrDefault(normalizedEmail);
if (userStamp == null) if (userStamp == null)
{ {
return Task.FromResult<IdentityUser>(null); return Task.FromResult<IdentityUser>(null);

View File

@ -20,8 +20,8 @@ public class Program
logging.AddSerilog(hostingContext, (e, globalSettings) => logging.AddSerilog(hostingContext, (e, globalSettings) =>
{ {
var context = e.Properties["SourceContext"].ToString(); var context = e.Properties["SourceContext"].ToString();
if (e.Properties.ContainsKey("RequestPath") && if (e.Properties.TryGetValue("RequestPath", out var requestPath) &&
!string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) && !string.IsNullOrWhiteSpace(requestPath?.ToString()) &&
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer"))) (context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
{ {
return false; return false;

View File

@ -29,12 +29,12 @@ public class AccessControlService : IAccessControlService
} }
var userRole = GetUserRoleFromClaim(); 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 false;
} }
return RolePermissionMapping.RolePermissions[userRole].Contains(permission); return rolePermissions.Contains(permission);
} }
public string GetUserRole(string userEmail) public string GetUserRole(string userEmail)

View File

@ -25,7 +25,7 @@ public class UpdateTwoFactorAuthenticatorRequestModel : SecretVerificationReques
{ {
providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>(); providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
} }
else if (providers.ContainsKey(TwoFactorProviderType.Authenticator)) else
{ {
providers.Remove(TwoFactorProviderType.Authenticator); providers.Remove(TwoFactorProviderType.Authenticator);
} }
@ -62,7 +62,7 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV
{ {
providers = []; providers = [];
} }
else if (providers.ContainsKey(TwoFactorProviderType.Duo)) else
{ {
providers.Remove(TwoFactorProviderType.Duo); providers.Remove(TwoFactorProviderType.Duo);
} }
@ -88,7 +88,7 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV
{ {
providers = []; providers = [];
} }
else if (providers.ContainsKey(TwoFactorProviderType.OrganizationDuo)) else
{ {
providers.Remove(TwoFactorProviderType.OrganizationDuo); providers.Remove(TwoFactorProviderType.OrganizationDuo);
} }
@ -145,7 +145,7 @@ public class UpdateTwoFactorYubicoOtpRequestModel : SecretVerificationRequestMod
{ {
providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>(); providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
} }
else if (providers.ContainsKey(TwoFactorProviderType.YubiKey)) else
{ {
providers.Remove(TwoFactorProviderType.YubiKey); providers.Remove(TwoFactorProviderType.YubiKey);
} }
@ -228,7 +228,7 @@ public class TwoFactorEmailRequestModel : SecretVerificationRequestModel
{ {
providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>(); providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
} }
else if (providers.ContainsKey(TwoFactorProviderType.Email)) else
{ {
providers.Remove(TwoFactorProviderType.Email); providers.Remove(TwoFactorProviderType.Email);
} }

View File

@ -13,9 +13,9 @@ public class TwoFactorAuthenticatorResponseModel : ResponseModel
ArgumentNullException.ThrowIfNull(user); ArgumentNullException.ThrowIfNull(user);
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator); 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; Enabled = provider.Enabled;
} }
else else

View File

@ -15,9 +15,9 @@ public class TwoFactorEmailResponseModel : ResponseModel
} }
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); 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; Enabled = provider.Enabled;
} }
else else

View File

@ -19,29 +19,29 @@ public class TwoFactorYubiKeyResponseModel : ResponseModel
{ {
Enabled = provider.Enabled; 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 else

View File

@ -6,14 +6,10 @@ using Bit.Api.Utilities;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; 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.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -161,8 +157,6 @@ public class AccountsController(
[HttpPost("cancel")] [HttpPost("cancel")]
public async Task PostCancelAsync( public async Task PostCancelAsync(
[FromBody] SubscriptionCancellationRequestModel request, [FromBody] SubscriptionCancellationRequestModel request,
[FromServices] ICurrentContext currentContext,
[FromServices] IReferenceEventService referenceEventService,
[FromServices] ISubscriberService subscriberService) [FromServices] ISubscriberService subscriberService)
{ {
var user = await userService.GetUserByPrincipalAsync(User); var user = await userService.GetUserByPrincipalAsync(User);
@ -175,12 +169,6 @@ public class AccountsController(
await subscriberService.CancelSubscription(user, await subscriberService.CancelSubscription(user,
new OffboardingSurveyResponse { UserId = user.Id, Reason = request.Reason, Feedback = request.Feedback }, new OffboardingSurveyResponse { UserId = user.Id, Reason = request.Reason, Feedback = request.Feedback },
user.IsExpired()); user.IsExpired());
await referenceEventService.RaiseEventAsync(new ReferenceEvent(
ReferenceEventType.CancelSubscription,
user,
currentContext)
{ EndOfPeriod = user.IsExpired() });
} }
[HttpPost("reinstate-premium")] [HttpPost("reinstate-premium")]

View File

@ -20,9 +20,6 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; 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.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -44,7 +41,6 @@ public class OrganizationsController(
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand, IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand,
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand, IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,
IReferenceEventService referenceEventService,
ISubscriberService subscriberService, ISubscriberService subscriberService,
IOrganizationInstallationRepository organizationInstallationRepository, IOrganizationInstallationRepository organizationInstallationRepository,
IPricingClient pricingClient) IPricingClient pricingClient)
@ -246,14 +242,6 @@ public class OrganizationsController(
Feedback = request.Feedback Feedback = request.Feedback
}, },
organization.IsExpired()); organization.IsExpired());
await referenceEventService.RaiseEventAsync(new ReferenceEvent(
ReferenceEventType.CancelSubscription,
organization,
currentContext)
{
EndOfPeriod = organization.IsExpired()
});
} }
[HttpPost("{id:guid}/reinstate")] [HttpPost("{id:guid}/reinstate")]

View File

@ -81,13 +81,6 @@ public class ProviderBillingController(
[FromRoute] Guid providerId, [FromRoute] Guid providerId,
[FromBody] UpdatePaymentMethodRequestBody requestBody) [FromBody] UpdatePaymentMethodRequestBody requestBody)
{ {
var allowProviderPaymentMethod = featureService.IsEnabled(FeatureFlagKeys.PM18794_ProviderPaymentMethod);
if (!allowProviderPaymentMethod)
{
return TypedResults.NotFound();
}
var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId); var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);
if (provider == null) if (provider == null)
@ -111,13 +104,6 @@ public class ProviderBillingController(
[FromRoute] Guid providerId, [FromRoute] Guid providerId,
[FromBody] VerifyBankAccountRequestBody requestBody) [FromBody] VerifyBankAccountRequestBody requestBody)
{ {
var allowProviderPaymentMethod = featureService.IsEnabled(FeatureFlagKeys.PM18794_ProviderPaymentMethod);
if (!allowProviderPaymentMethod)
{
return TypedResults.NotFound();
}
var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId); var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);
if (provider == null) if (provider == null)

View File

@ -5,7 +5,6 @@ using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Identity; using Bit.Core.Identity;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.AuthorizationRequirements; using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Entities; 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.Queries.Secrets.Interfaces;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services; 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.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -30,7 +26,6 @@ public class SecretsController : Controller
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IProjectRepository _projectRepository; private readonly IProjectRepository _projectRepository;
private readonly ISecretRepository _secretRepository; private readonly ISecretRepository _secretRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly ICreateSecretCommand _createSecretCommand; private readonly ICreateSecretCommand _createSecretCommand;
private readonly IUpdateSecretCommand _updateSecretCommand; private readonly IUpdateSecretCommand _updateSecretCommand;
private readonly IDeleteSecretCommand _deleteSecretCommand; private readonly IDeleteSecretCommand _deleteSecretCommand;
@ -39,14 +34,12 @@ public class SecretsController : Controller
private readonly ISecretAccessPoliciesUpdatesQuery _secretAccessPoliciesUpdatesQuery; private readonly ISecretAccessPoliciesUpdatesQuery _secretAccessPoliciesUpdatesQuery;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IEventService _eventService; private readonly IEventService _eventService;
private readonly IReferenceEventService _referenceEventService;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
public SecretsController( public SecretsController(
ICurrentContext currentContext, ICurrentContext currentContext,
IProjectRepository projectRepository, IProjectRepository projectRepository,
ISecretRepository secretRepository, ISecretRepository secretRepository,
IOrganizationRepository organizationRepository,
ICreateSecretCommand createSecretCommand, ICreateSecretCommand createSecretCommand,
IUpdateSecretCommand updateSecretCommand, IUpdateSecretCommand updateSecretCommand,
IDeleteSecretCommand deleteSecretCommand, IDeleteSecretCommand deleteSecretCommand,
@ -55,13 +48,11 @@ public class SecretsController : Controller
ISecretAccessPoliciesUpdatesQuery secretAccessPoliciesUpdatesQuery, ISecretAccessPoliciesUpdatesQuery secretAccessPoliciesUpdatesQuery,
IUserService userService, IUserService userService,
IEventService eventService, IEventService eventService,
IReferenceEventService referenceEventService,
IAuthorizationService authorizationService) IAuthorizationService authorizationService)
{ {
_currentContext = currentContext; _currentContext = currentContext;
_projectRepository = projectRepository; _projectRepository = projectRepository;
_secretRepository = secretRepository; _secretRepository = secretRepository;
_organizationRepository = organizationRepository;
_createSecretCommand = createSecretCommand; _createSecretCommand = createSecretCommand;
_updateSecretCommand = updateSecretCommand; _updateSecretCommand = updateSecretCommand;
_deleteSecretCommand = deleteSecretCommand; _deleteSecretCommand = deleteSecretCommand;
@ -70,7 +61,6 @@ public class SecretsController : Controller
_secretAccessPoliciesUpdatesQuery = secretAccessPoliciesUpdatesQuery; _secretAccessPoliciesUpdatesQuery = secretAccessPoliciesUpdatesQuery;
_userService = userService; _userService = userService;
_eventService = eventService; _eventService = eventService;
_referenceEventService = referenceEventService;
_authorizationService = authorizationService; _authorizationService = authorizationService;
} }
@ -148,9 +138,6 @@ public class SecretsController : Controller
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount) if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount)
{ {
await _eventService.LogServiceAccountSecretEventAsync(userId, secret, EventType.Secret_Retrieved); 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); return new SecretResponseModel(secret, access.Read, access.Write);
@ -266,7 +253,7 @@ public class SecretsController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
await LogSecretsRetrievalAsync(secrets.First().OrganizationId, secrets); await LogSecretsRetrievalAsync(secrets);
var responses = secrets.Select(s => new BaseSecretResponseModel(s)); var responses = secrets.Select(s => new BaseSecretResponseModel(s));
return new ListResponseModel<BaseSecretResponseModel>(responses); return new ListResponseModel<BaseSecretResponseModel>(responses);
@ -303,21 +290,18 @@ public class SecretsController : Controller
if (syncResult.HasChanges) if (syncResult.HasChanges)
{ {
await LogSecretsRetrievalAsync(organizationId, syncResult.Secrets); await LogSecretsRetrievalAsync(syncResult.Secrets);
} }
return new SecretsSyncResponseModel(syncResult.HasChanges, syncResult.Secrets); return new SecretsSyncResponseModel(syncResult.HasChanges, syncResult.Secrets);
} }
private async Task LogSecretsRetrievalAsync(Guid organizationId, IEnumerable<Secret> secrets) private async Task LogSecretsRetrievalAsync(IEnumerable<Secret> secrets)
{ {
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount) if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount)
{ {
var userId = _userService.GetProperUserId(User)!.Value; var userId = _userService.GetProperUserId(User)!.Value;
var org = await _organizationRepository.GetByIdAsync(organizationId);
await _eventService.LogServiceAccountSecretsEventAsync(userId, secrets, EventType.Secret_Retrieved); await _eventService.LogServiceAccountSecretsEventAsync(userId, secrets, EventType.Secret_Retrieved);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.SmServiceAccountAccessedSecret, org, _currentContext));
} }
} }
} }

View File

@ -5,7 +5,6 @@ using Bit.Api.Tools.Models.Request;
using Bit.Api.Tools.Models.Response; using Bit.Api.Tools.Models.Response;
using Bit.Api.Utilities; using Bit.Api.Utilities;
using Bit.Core; using Bit.Core;
using Bit.Core.Context;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
@ -33,7 +32,6 @@ public class SendsController : Controller
private readonly INonAnonymousSendCommand _nonAnonymousSendCommand; private readonly INonAnonymousSendCommand _nonAnonymousSendCommand;
private readonly ILogger<SendsController> _logger; private readonly ILogger<SendsController> _logger;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly ICurrentContext _currentContext;
public SendsController( public SendsController(
ISendRepository sendRepository, ISendRepository sendRepository,
@ -43,8 +41,7 @@ public class SendsController : Controller
INonAnonymousSendCommand nonAnonymousSendCommand, INonAnonymousSendCommand nonAnonymousSendCommand,
ISendFileStorageService sendFileStorageService, ISendFileStorageService sendFileStorageService,
ILogger<SendsController> logger, ILogger<SendsController> logger,
GlobalSettings globalSettings, GlobalSettings globalSettings)
ICurrentContext currentContext)
{ {
_sendRepository = sendRepository; _sendRepository = sendRepository;
_userService = userService; _userService = userService;
@ -54,7 +51,6 @@ public class SendsController : Controller
_sendFileStorageService = sendFileStorageService; _sendFileStorageService = sendFileStorageService;
_logger = logger; _logger = logger;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_currentContext = currentContext;
} }
#region Anonymous endpoints #region Anonymous endpoints

View File

@ -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);
} }
} }

View File

@ -1064,7 +1064,7 @@ public class CiphersController : Controller
[HttpPut("share")] [HttpPut("share")]
[HttpPost("share")] [HttpPost("share")]
public async Task PutShareMany([FromBody] CipherBulkShareRequestModel model) public async Task<ListResponseModel<CipherMiniResponseModel>> PutShareMany([FromBody] CipherBulkShareRequestModel model)
{ {
var organizationId = new Guid(model.Ciphers.First().OrganizationId); var organizationId = new Guid(model.Ciphers.First().OrganizationId);
if (!await _currentContext.OrganizationUser(organizationId)) if (!await _currentContext.OrganizationUser(organizationId))
@ -1073,38 +1073,41 @@ public class CiphersController : Controller
} }
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false); var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false);
var ciphersDict = ciphers.ToDictionary(c => c.Id); var ciphersDict = ciphers.ToDictionary(c => c.Id);
// Validate the model was encrypted for the posting user // Validate the model was encrypted for the posting user
foreach (var cipher in model.Ciphers) 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) 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); ValidateClientVersionForFido2CredentialSupport(existingCipher);
shareCiphers.Add((cipher.ToCipher(existingCipher), cipher.LastKnownRevisionDate)); shareCiphers.Add((cipher.ToCipherDetails(existingCipher), cipher.LastKnownRevisionDate));
} }
await _cipherService.ShareManyAsync(shareCiphers, organizationId, var updated = await _cipherService.ShareManyAsync(
model.CollectionIds.Select(c => new Guid(c)), userId); shareCiphers,
organizationId,
model.CollectionIds.Select(Guid.Parse),
userId
);
var response = updated.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp));
return new ListResponseModel<CipherMiniResponseModel>(response);
} }
[HttpPost("purge")] [HttpPost("purge")]
@ -1186,14 +1189,14 @@ public class CiphersController : Controller
var cipher = await GetByIdAsync(id, userId); var cipher = await GetByIdAsync(id, userId);
var attachments = cipher?.GetAttachments(); 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(); throw new NotFoundException();
} }
return new AttachmentUploadDataResponseModel return new AttachmentUploadDataResponseModel
{ {
Url = await _attachmentStorageService.GetAttachmentUploadUrlAsync(cipher, attachments[attachmentId]), Url = await _attachmentStorageService.GetAttachmentUploadUrlAsync(cipher, attachment),
FileUploadType = _attachmentStorageService.FileUploadType, FileUploadType = _attachmentStorageService.FileUploadType,
}; };
} }
@ -1212,11 +1215,10 @@ public class CiphersController : Controller
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var cipher = await GetByIdAsync(id, userId); var cipher = await GetByIdAsync(id, userId);
var attachments = cipher?.GetAttachments(); var attachments = cipher?.GetAttachments();
if (attachments == null || !attachments.ContainsKey(attachmentId)) if (attachments == null || !attachments.TryGetValue(attachmentId, out var attachmentData))
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
var attachmentData = attachments[attachmentId];
await Request.GetFileAsync(async (stream) => await Request.GetFileAsync(async (stream) =>
{ {
@ -1366,7 +1368,7 @@ public class CiphersController : Controller
var cipher = await _cipherRepository.GetByIdAsync(new Guid(cipherId)); var cipher = await _cipherRepository.GetByIdAsync(new Guid(cipherId));
var attachments = cipher?.GetAttachments() ?? new Dictionary<string, CipherAttachment.MetaData>(); var attachments = cipher?.GetAttachments() ?? new Dictionary<string, CipherAttachment.MetaData>();
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) if (_attachmentStorageService is AzureSendFileStorageService azureFileStorageService)
{ {
@ -1376,7 +1378,7 @@ public class CiphersController : Controller
return; return;
} }
await _cipherService.ValidateCipherAttachmentFile(cipher, attachments[attachmentId]); await _cipherService.ValidateCipherAttachmentFile(cipher, attachment);
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -113,18 +113,25 @@ public class CipherRequestModel
if (hasAttachments2) 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.FileName = attachment2.FileName;
attachment.Value.Key = attachment2.Key; attachment.Value.Key = attachment2.Key;
} }
} }
else if (hasAttachments) 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; attachment.Value.Key = null;
} }
} }

View File

@ -129,13 +129,13 @@ public class CipherDetailsResponseModel : CipherResponseModel
IDictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphers, string obj = "cipherDetails") IDictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphers, string obj = "cipherDetails")
: base(cipher, user, organizationAbilities, globalSettings, obj) : 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 else
{ {
CollectionIds = new Guid[] { }; CollectionIds = [];
} }
} }
@ -147,7 +147,7 @@ public class CipherDetailsResponseModel : CipherResponseModel
IEnumerable<CollectionCipher> collectionCiphers, string obj = "cipherDetails") IEnumerable<CollectionCipher> collectionCiphers, string obj = "cipherDetails")
: base(cipher, user, organizationAbilities, globalSettings, obj) : base(cipher, user, organizationAbilities, globalSettings, obj)
{ {
CollectionIds = collectionCiphers?.Select(c => c.CollectionId) ?? new List<Guid>(); CollectionIds = collectionCiphers?.Select(c => c.CollectionId) ?? [];
} }
public CipherDetailsResponseModel( public CipherDetailsResponseModel(
@ -158,7 +158,7 @@ public class CipherDetailsResponseModel : CipherResponseModel
string obj = "cipherDetails") string obj = "cipherDetails")
: base(cipher, user, organizationAbilities, globalSettings, obj) : base(cipher, user, organizationAbilities, globalSettings, obj)
{ {
CollectionIds = cipher.CollectionIds ?? new List<Guid>(); CollectionIds = cipher.CollectionIds ?? [];
} }
public IEnumerable<Guid> CollectionIds { get; set; } public IEnumerable<Guid> CollectionIds { get; set; }
@ -170,13 +170,13 @@ public class CipherMiniDetailsResponseModel : CipherMiniResponseModel
IDictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphers, bool orgUseTotp, string obj = "cipherMiniDetails") IDictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphers, bool orgUseTotp, string obj = "cipherMiniDetails")
: base(cipher, globalSettings, orgUseTotp, obj) : 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 else
{ {
CollectionIds = new Guid[] { }; CollectionIds = [];
} }
} }
@ -184,7 +184,7 @@ public class CipherMiniDetailsResponseModel : CipherMiniResponseModel
GlobalSettings globalSettings, bool orgUseTotp, string obj = "cipherMiniDetails") GlobalSettings globalSettings, bool orgUseTotp, string obj = "cipherMiniDetails")
: base(cipher, globalSettings, orgUseTotp, obj) : base(cipher, globalSettings, orgUseTotp, obj)
{ {
CollectionIds = cipher.CollectionIds ?? new List<Guid>(); CollectionIds = cipher.CollectionIds ?? [];
} }
public CipherMiniDetailsResponseModel(CipherOrganizationDetailsWithCollections cipher, public CipherMiniDetailsResponseModel(CipherOrganizationDetailsWithCollections cipher,

View File

@ -28,8 +28,8 @@ public class AppleController : Controller
return new BadRequestResult(); return new BadRequestResult();
} }
var key = HttpContext.Request.Query.ContainsKey("key") ? var key = HttpContext.Request.Query.TryGetValue("key", out var keyValue) ?
HttpContext.Request.Query["key"].ToString() : null; keyValue.ToString() : null;
if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.AppleWebhookKey)) if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.AppleWebhookKey))
{ {
return new BadRequestResult(); return new BadRequestResult();

View File

@ -51,8 +51,8 @@ public class PayPalController : Controller
[HttpPost("ipn")] [HttpPost("ipn")]
public async Task<IActionResult> PostIpn() public async Task<IActionResult> PostIpn()
{ {
var key = HttpContext.Request.Query.ContainsKey("key") var key = HttpContext.Request.Query.TryGetValue("key", out var keyValue)
? HttpContext.Request.Query["key"].ToString() ? keyValue.ToString()
: null; : null;
if (string.IsNullOrEmpty(key)) if (string.IsNullOrEmpty(key))

View File

@ -20,8 +20,8 @@ public class Program
return e.Level >= globalSettings.MinLogLevel.BillingSettings.Jobs; return e.Level >= globalSettings.MinLogLevel.BillingSettings.Jobs;
} }
if (e.Properties.ContainsKey("RequestPath") && if (e.Properties.TryGetValue("RequestPath", out var requestPath) &&
!string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) && !string.IsNullOrWhiteSpace(requestPath?.ToString()) &&
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer"))) (context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
{ {
return false; return false;

View File

@ -1,8 +1,4 @@
using Bit.Core.Context; using Bit.Core.Repositories;
using Bit.Core.Repositories;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Event = Stripe.Event; using Event = Stripe.Event;
namespace Bit.Billing.Services.Implementations; namespace Bit.Billing.Services.Implementations;
@ -10,23 +6,17 @@ namespace Bit.Billing.Services.Implementations;
public class CustomerUpdatedHandler : ICustomerUpdatedHandler public class CustomerUpdatedHandler : ICustomerUpdatedHandler
{ {
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IReferenceEventService _referenceEventService;
private readonly ICurrentContext _currentContext;
private readonly IStripeEventService _stripeEventService; private readonly IStripeEventService _stripeEventService;
private readonly IStripeEventUtilityService _stripeEventUtilityService; private readonly IStripeEventUtilityService _stripeEventUtilityService;
private readonly ILogger<CustomerUpdatedHandler> _logger; private readonly ILogger<CustomerUpdatedHandler> _logger;
public CustomerUpdatedHandler( public CustomerUpdatedHandler(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IReferenceEventService referenceEventService,
ICurrentContext currentContext,
IStripeEventService stripeEventService, IStripeEventService stripeEventService,
IStripeEventUtilityService stripeEventUtilityService, IStripeEventUtilityService stripeEventUtilityService,
ILogger<CustomerUpdatedHandler> logger) ILogger<CustomerUpdatedHandler> logger)
{ {
_organizationRepository = organizationRepository ?? throw new ArgumentNullException(nameof(organizationRepository)); _organizationRepository = organizationRepository ?? throw new ArgumentNullException(nameof(organizationRepository));
_referenceEventService = referenceEventService;
_currentContext = currentContext;
_stripeEventService = stripeEventService; _stripeEventService = stripeEventService;
_stripeEventUtilityService = stripeEventUtilityService; _stripeEventUtilityService = stripeEventUtilityService;
_logger = logger; _logger = logger;
@ -95,20 +85,5 @@ public class CustomerUpdatedHandler : ICustomerUpdatedHandler
organization.BillingEmail = customer.Email; organization.BillingEmail = customer.Email;
await _organizationRepository.ReplaceAsync(organization); 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));
} }
} }

View File

@ -3,13 +3,9 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Context;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Event = Stripe.Event; using Event = Stripe.Event;
namespace Bit.Billing.Services.Implementations; namespace Bit.Billing.Services.Implementations;
@ -22,9 +18,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
private readonly IStripeFacade _stripeFacade; private readonly IStripeFacade _stripeFacade;
private readonly IProviderRepository _providerRepository; private readonly IProviderRepository _providerRepository;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IReferenceEventService _referenceEventService;
private readonly ICurrentContext _currentContext;
private readonly IUserRepository _userRepository;
private readonly IStripeEventUtilityService _stripeEventUtilityService; private readonly IStripeEventUtilityService _stripeEventUtilityService;
private readonly IPushNotificationService _pushNotificationService; private readonly IPushNotificationService _pushNotificationService;
private readonly IOrganizationEnableCommand _organizationEnableCommand; private readonly IOrganizationEnableCommand _organizationEnableCommand;
@ -36,9 +29,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
IStripeFacade stripeFacade, IStripeFacade stripeFacade,
IProviderRepository providerRepository, IProviderRepository providerRepository,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IReferenceEventService referenceEventService,
ICurrentContext currentContext,
IUserRepository userRepository,
IStripeEventUtilityService stripeEventUtilityService, IStripeEventUtilityService stripeEventUtilityService,
IUserService userService, IUserService userService,
IPushNotificationService pushNotificationService, IPushNotificationService pushNotificationService,
@ -50,9 +40,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
_stripeFacade = stripeFacade; _stripeFacade = stripeFacade;
_providerRepository = providerRepository; _providerRepository = providerRepository;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_referenceEventService = referenceEventService;
_currentContext = currentContext;
_userRepository = userRepository;
_stripeEventUtilityService = stripeEventUtilityService; _stripeEventUtilityService = stripeEventUtilityService;
_userService = userService; _userService = userService;
_pushNotificationService = pushNotificationService; _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", _logger.LogError("invoice.payment_succeeded webhook ({EventID}) for Provider ({ProviderID}) indicates missing subscription line items",
parsedEvent.Id, parsedEvent.Id,
provider.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) else if (organizationId.HasValue)
{ {
@ -156,15 +123,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
await _organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd); await _organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
await _pushNotificationService.PushSyncOrganizationStatusAsync(organization); 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) else if (userId.HasValue)
{ {
@ -174,14 +132,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
} }
await _userService.EnablePremiumAsync(userId.Value, subscription.CurrentPeriodEnd); 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,
});
} }
} }
} }

View File

@ -8,14 +8,13 @@ using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Tools.Entities;
using Bit.Core.Utilities; using Bit.Core.Utilities;
#nullable enable #nullable enable
namespace Bit.Core.AdminConsole.Entities; namespace Bit.Core.AdminConsole.Entities;
public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable, IReferenceable public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable
{ {
private Dictionary<TwoFactorProviderType, TwoFactorProvider>? _twoFactorProviders; private Dictionary<TwoFactorProviderType, TwoFactorProvider>? _twoFactorProviders;
@ -258,12 +257,12 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
public bool TwoFactorProviderIsEnabled(TwoFactorProviderType provider) public bool TwoFactorProviderIsEnabled(TwoFactorProviderType provider)
{ {
var providers = GetTwoFactorProviders(); var providers = GetTwoFactorProviders();
if (providers == null || !providers.ContainsKey(provider)) if (providers == null || !providers.TryGetValue(provider, out var twoFactorProvider))
{ {
return false; return false;
} }
return providers[provider].Enabled && Use2fa; return twoFactorProvider.Enabled && Use2fa;
} }
public bool TwoFactorIsEnabled() public bool TwoFactorIsEnabled()
@ -280,12 +279,7 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider) public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider)
{ {
var providers = GetTwoFactorProviders(); var providers = GetTwoFactorProviders();
if (providers == null || !providers.ContainsKey(provider)) return providers?.GetValueOrDefault(provider);
{
return null;
}
return providers[provider];
} }
public void UpdateFromLicense(OrganizationLicense license, IFeatureService featureService) public void UpdateFromLicense(OrganizationLicense license, IFeatureService featureService)

View File

@ -1,12 +1,15 @@
using Bit.Core.Enums; #nullable enable
using Bit.Core.Enums;
namespace Bit.Core.AdminConsole.Models.Data.Integrations; namespace Bit.Core.AdminConsole.Models.Data.Integrations;
public interface IIntegrationMessage public interface IIntegrationMessage
{ {
IntegrationType IntegrationType { get; } IntegrationType IntegrationType { get; }
int RetryCount { get; set; } string MessageId { get; set; }
DateTime? DelayUntilDate { get; set; } int RetryCount { get; }
DateTime? DelayUntilDate { get; }
void ApplyRetry(DateTime? handlerDelayUntilDate); void ApplyRetry(DateTime? handlerDelayUntilDate);
string ToJson(); string ToJson();
} }

View File

@ -1,4 +1,6 @@
namespace Bit.Core.AdminConsole.Models.Data.Integrations; #nullable enable
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
public class IntegrationHandlerResult public class IntegrationHandlerResult
{ {

View File

@ -1,13 +1,15 @@
using System.Text.Json; #nullable enable
using System.Text.Json;
using Bit.Core.Enums; using Bit.Core.Enums;
namespace Bit.Core.AdminConsole.Models.Data.Integrations; namespace Bit.Core.AdminConsole.Models.Data.Integrations;
public class IntegrationMessage<T> : IIntegrationMessage public class IntegrationMessage : IIntegrationMessage
{ {
public IntegrationType IntegrationType { get; set; } public IntegrationType IntegrationType { get; set; }
public T Configuration { get; set; } public required string MessageId { get; set; }
public string RenderedTemplate { get; set; } public required string RenderedTemplate { get; set; }
public int RetryCount { get; set; } = 0; public int RetryCount { get; set; } = 0;
public DateTime? DelayUntilDate { get; set; } public DateTime? DelayUntilDate { get; set; }
@ -22,12 +24,22 @@ public class IntegrationMessage<T> : IIntegrationMessage
DelayUntilDate = baseTime.AddSeconds(backoffSeconds + jitterSeconds); DelayUntilDate = baseTime.AddSeconds(backoffSeconds + jitterSeconds);
} }
public string ToJson() public virtual string ToJson()
{
return JsonSerializer.Serialize(this);
}
}
public class IntegrationMessage<T> : IntegrationMessage
{
public required T Configuration { get; set; }
public override string ToJson()
{ {
return JsonSerializer.Serialize(this); return JsonSerializer.Serialize(this);
} }
public static IntegrationMessage<T> FromJson(string json) public static IntegrationMessage<T>? FromJson(string json)
{ {
return JsonSerializer.Deserialize<IntegrationMessage<T>>(json); return JsonSerializer.Deserialize<IntegrationMessage<T>>(json);
} }

View File

@ -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); public record SlackIntegration(string token);

View File

@ -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); public record SlackIntegrationConfiguration(string channelId);

View File

@ -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); public record SlackIntegrationConfigurationDetails(string channelId, string token);

View File

@ -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); public record WebhookIntegrationConfiguration(string url);

View File

@ -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); public record WebhookIntegrationConfigurationDetails(string url);

View File

@ -1,4 +1,5 @@
 #nullable enable
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Bit.Core.Models.Slack; namespace Bit.Core.Models.Slack;

View File

@ -1,15 +1,11 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; 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; namespace Bit.Core.AdminConsole.OrganizationFeatures.Groups;
@ -18,21 +14,16 @@ public class CreateGroupCommand : ICreateGroupCommand
private readonly IEventService _eventService; private readonly IEventService _eventService;
private readonly IGroupRepository _groupRepository; private readonly IGroupRepository _groupRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IReferenceEventService _referenceEventService;
private readonly ICurrentContext _currentContext;
public CreateGroupCommand( public CreateGroupCommand(
IEventService eventService, IEventService eventService,
IGroupRepository groupRepository, IGroupRepository groupRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository
IReferenceEventService referenceEventService, )
ICurrentContext currentContext)
{ {
_eventService = eventService; _eventService = eventService;
_groupRepository = groupRepository; _groupRepository = groupRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_referenceEventService = referenceEventService;
_currentContext = currentContext;
} }
public async Task CreateGroupAsync(Group group, Organization organization, public async Task CreateGroupAsync(Group group, Organization organization,
@ -77,8 +68,6 @@ public class CreateGroupCommand : ICreateGroupCommand
{ {
await _groupRepository.CreateAsync(group, collections); await _groupRepository.CreateAsync(group, collections);
} }
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.GroupCreated, organization, _currentContext));
} }
private async Task GroupRepositoryUpdateUsersAsync(Group group, IEnumerable<Guid> userIds, private async Task GroupRepositoryUpdateUsersAsync(Group group, IEnumerable<Guid> userIds,

View File

@ -7,9 +7,6 @@ using Bit.Core.Exceptions;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
#nullable enable #nullable enable
@ -24,7 +21,6 @@ public class DeleteClaimedOrganizationUserAccountCommand : IDeleteClaimedOrganiz
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
private readonly IReferenceEventService _referenceEventService;
private readonly IPushNotificationService _pushService; private readonly IPushNotificationService _pushService;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IProviderUserRepository _providerUserRepository; private readonly IProviderUserRepository _providerUserRepository;
@ -36,7 +32,6 @@ public class DeleteClaimedOrganizationUserAccountCommand : IDeleteClaimedOrganiz
IUserRepository userRepository, IUserRepository userRepository,
ICurrentContext currentContext, ICurrentContext currentContext,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IReferenceEventService referenceEventService,
IPushNotificationService pushService, IPushNotificationService pushService,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IProviderUserRepository providerUserRepository) IProviderUserRepository providerUserRepository)
@ -48,7 +43,6 @@ public class DeleteClaimedOrganizationUserAccountCommand : IDeleteClaimedOrganiz
_userRepository = userRepository; _userRepository = userRepository;
_currentContext = currentContext; _currentContext = currentContext;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_referenceEventService = referenceEventService;
_pushService = pushService; _pushService = pushService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_providerUserRepository = providerUserRepository; _providerUserRepository = providerUserRepository;
@ -195,8 +189,6 @@ public class DeleteClaimedOrganizationUserAccountCommand : IDeleteClaimedOrganiz
await _userRepository.DeleteManyAsync(users); await _userRepository.DeleteManyAsync(users);
foreach (var user in users) foreach (var user in users)
{ {
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.DeleteAccount, user, _currentContext));
await _pushService.PushLogOutAsync(user.Id); await _pushService.PushLogOutAsync(user.Id);
} }

View File

@ -9,15 +9,11 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Utilities.Commands; using Bit.Core.AdminConsole.Utilities.Commands;
using Bit.Core.AdminConsole.Utilities.Errors; using Bit.Core.AdminConsole.Utilities.Errors;
using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.AdminConsole.Utilities.Validation;
using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; 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 Microsoft.Extensions.Logging;
using OrganizationUserInvite = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite; using OrganizationUserInvite = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite;
@ -28,8 +24,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
IInviteUsersValidator inviteUsersValidator, IInviteUsersValidator inviteUsersValidator,
IPaymentService paymentService, IPaymentService paymentService,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IReferenceEventService referenceEventService,
ICurrentContext currentContext,
IApplicationCacheService applicationCacheService, IApplicationCacheService applicationCacheService,
IMailService mailService, IMailService mailService,
ILogger<InviteOrganizationUsersCommand> logger, ILogger<InviteOrganizationUsersCommand> logger,
@ -121,8 +115,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
await SendAdditionalEmailsAsync(validatedRequest, organization); await SendAdditionalEmailsAsync(validatedRequest, organization);
await SendInvitesAsync(organizationUserToInviteEntities, organization); await SendInvitesAsync(organizationUserToInviteEntities, organization);
await PublishReferenceEventAsync(validatedRequest, organization);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -190,14 +182,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
} }
} }
private async Task PublishReferenceEventAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult,
Organization organization) =>
await referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.InvitedUsers, organization, currentContext)
{
Users = validatedResult.Value.Invites.Length
});
private async Task SendInvitesAsync(IEnumerable<CreateOrganizationUser> users, Organization organization) => private async Task SendInvitesAsync(IEnumerable<CreateOrganizationUser> users, Organization organization) =>
await sendOrganizationInvitesCommand.SendInvitesAsync( await sendOrganizationInvitesCommand.SendInvitesAsync(
new SendInvitesRequest( new SendInvitesRequest(
@ -284,15 +268,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
await organizationRepository.ReplaceAsync(organization); // could optimize this with only a property update await organizationRepository.ReplaceAsync(organization); // could optimize this with only a property update
await applicationCacheService.UpsertOrganizationAbilityAsync(organization); 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
});
} }
} }
} }

View File

@ -5,7 +5,6 @@ using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -15,9 +14,6 @@ using Bit.Core.Models.StaticStore;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; 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.Core.Utilities;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
@ -36,8 +32,6 @@ public class CloudOrganizationSignUpCommand(
IOrganizationBillingService organizationBillingService, IOrganizationBillingService organizationBillingService,
IPaymentService paymentService, IPaymentService paymentService,
IPolicyService policyService, IPolicyService policyService,
IReferenceEventService referenceEventService,
ICurrentContext currentContext,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationApiKeyRepository organizationApiKeyRepository, IOrganizationApiKeyRepository organizationApiKeyRepository,
IApplicationCacheService applicationCacheService, IApplicationCacheService applicationCacheService,
@ -132,17 +126,6 @@ public class CloudOrganizationSignUpCommand(
var ownerId = signup.IsFromProvider ? default : signup.Owner.Id; var ownerId = signup.IsFromProvider ? default : signup.Owner.Id;
var returnValue = await SignUpAsync(organization, ownerId, signup.OwnerKey, signup.CollectionName, true); 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); return new SignUpOrganizationResponse(returnValue.organization, returnValue.organizationUser);
} }

View File

@ -2,38 +2,28 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; 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; namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
public class OrganizationDeleteCommand : IOrganizationDeleteCommand public class OrganizationDeleteCommand : IOrganizationDeleteCommand
{ {
private readonly IApplicationCacheService _applicationCacheService; private readonly IApplicationCacheService _applicationCacheService;
private readonly ICurrentContext _currentContext;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IPaymentService _paymentService; private readonly IPaymentService _paymentService;
private readonly IReferenceEventService _referenceEventService;
private readonly ISsoConfigRepository _ssoConfigRepository; private readonly ISsoConfigRepository _ssoConfigRepository;
public OrganizationDeleteCommand( public OrganizationDeleteCommand(
IApplicationCacheService applicationCacheService, IApplicationCacheService applicationCacheService,
ICurrentContext currentContext,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IPaymentService paymentService, IPaymentService paymentService,
IReferenceEventService referenceEventService,
ISsoConfigRepository ssoConfigRepository) ISsoConfigRepository ssoConfigRepository)
{ {
_applicationCacheService = applicationCacheService; _applicationCacheService = applicationCacheService;
_currentContext = currentContext;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_paymentService = paymentService; _paymentService = paymentService;
_referenceEventService = referenceEventService;
_ssoConfigRepository = ssoConfigRepository; _ssoConfigRepository = ssoConfigRepository;
} }
@ -48,8 +38,6 @@ public class OrganizationDeleteCommand : IOrganizationDeleteCommand
var eop = !organization.ExpirationDate.HasValue || var eop = !organization.ExpirationDate.HasValue ||
organization.ExpirationDate.Value >= DateTime.UtcNow; organization.ExpirationDate.Value >= DateTime.UtcNow;
await _paymentService.CancelSubscriptionAsync(organization, eop); await _paymentService.CancelSubscriptionAsync(organization, eop);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.DeleteAccount, organization, _currentContext));
} }
catch (GatewayException) { } catch (GatewayException) { }
} }

View File

@ -8,9 +8,6 @@ using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore; using Bit.Core.Models.StaticStore;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; 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.Core.Utilities;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
@ -37,7 +34,6 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IPricingClient _pricingClient; private readonly IPricingClient _pricingClient;
private readonly IReferenceEventService _referenceEventService;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
private readonly IApplicationCacheService _applicationCacheService; private readonly IApplicationCacheService _applicationCacheService;
@ -46,7 +42,6 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati
public ProviderClientOrganizationSignUpCommand( public ProviderClientOrganizationSignUpCommand(
ICurrentContext currentContext, ICurrentContext currentContext,
IPricingClient pricingClient, IPricingClient pricingClient,
IReferenceEventService referenceEventService,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationApiKeyRepository organizationApiKeyRepository, IOrganizationApiKeyRepository organizationApiKeyRepository,
IApplicationCacheService applicationCacheService, IApplicationCacheService applicationCacheService,
@ -54,7 +49,6 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati
{ {
_currentContext = currentContext; _currentContext = currentContext;
_pricingClient = pricingClient; _pricingClient = pricingClient;
_referenceEventService = referenceEventService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationApiKeyRepository = organizationApiKeyRepository; _organizationApiKeyRepository = organizationApiKeyRepository;
_applicationCacheService = applicationCacheService; _applicationCacheService = applicationCacheService;
@ -108,16 +102,6 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati
var returnValue = await SignUpAsync(organization, signup.CollectionName); 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; return returnValue;
} }

View File

@ -104,8 +104,8 @@ public class SavePolicyCommand : ISavePolicyCommand
var dependentPolicyTypes = _policyValidators.Values var dependentPolicyTypes = _policyValidators.Values
.Where(otherValidator => otherValidator.RequiredPolicies.Contains(policyUpdate.Type)) .Where(otherValidator => otherValidator.RequiredPolicies.Contains(policyUpdate.Type))
.Select(otherValidator => otherValidator.Type) .Select(otherValidator => otherValidator.Type)
.Where(otherPolicyType => savedPoliciesDict.ContainsKey(otherPolicyType) && .Where(otherPolicyType => savedPoliciesDict.TryGetValue(otherPolicyType, out var savedPolicy) &&
savedPoliciesDict[otherPolicyType].Enabled) savedPolicy.Enabled)
.ToList(); .ToList();
switch (dependentPolicyTypes) switch (dependentPolicyTypes)

View File

@ -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; namespace Bit.Core.Services;
public abstract class EventLoggingListenerService : BackgroundService public abstract class EventLoggingListenerService : BackgroundService
{ {
protected readonly IEventMessageHandler _handler; protected readonly IEventMessageHandler _handler;
protected ILogger<EventLoggingListenerService> _logger;
protected EventLoggingListenerService(IEventMessageHandler handler) protected EventLoggingListenerService(IEventMessageHandler handler, ILogger<EventLoggingListenerService> 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<IEnumerable<EventMessage>>();
await _handler.HandleManyEventsAsync(eventMessages);
}
else if (root.ValueKind == JsonValueKind.Object)
{
var eventMessage = root.Deserialize<EventMessage>();
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"
);
}
}
} }
} }

View File

@ -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);
}

View File

@ -2,7 +2,8 @@
namespace Bit.Core.Services; namespace Bit.Core.Services;
public interface IIntegrationPublisher public interface IEventIntegrationPublisher : IAsyncDisposable
{ {
Task PublishAsync(IIntegrationMessage message); Task PublishAsync(IIntegrationMessage message);
Task PublishEventAsync(string body);
} }

View File

@ -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<IChannel> 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);
}

View File

@ -1,7 +1,7 @@
using System.Text; #nullable enable
using System.Text.Json;
using System.Text;
using Azure.Messaging.ServiceBus; using Azure.Messaging.ServiceBus;
using Bit.Core.Models.Data;
using Bit.Core.Settings; using Bit.Core.Settings;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -9,54 +9,31 @@ namespace Bit.Core.Services;
public class AzureServiceBusEventListenerService : EventLoggingListenerService public class AzureServiceBusEventListenerService : EventLoggingListenerService
{ {
private readonly ILogger<AzureServiceBusEventListenerService> _logger;
private readonly ServiceBusClient _client;
private readonly ServiceBusProcessor _processor; private readonly ServiceBusProcessor _processor;
public AzureServiceBusEventListenerService( public AzureServiceBusEventListenerService(
IEventMessageHandler handler, IEventMessageHandler handler,
ILogger<AzureServiceBusEventListenerService> logger, IAzureServiceBusService serviceBusService,
string subscriptionName,
GlobalSettings globalSettings, GlobalSettings globalSettings,
string subscriptionName) : base(handler) ILogger<AzureServiceBusEventListenerService> logger) : base(handler, logger)
{ {
_client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); _processor = serviceBusService.CreateProcessor(
_processor = _client.CreateProcessor(globalSettings.EventLogging.AzureServiceBus.EventTopicName, subscriptionName, new ServiceBusProcessorOptions()); globalSettings.EventLogging.AzureServiceBus.EventTopicName,
subscriptionName,
new ServiceBusProcessorOptions());
_logger = logger; _logger = logger;
} }
protected override async Task ExecuteAsync(CancellationToken cancellationToken) protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{ {
_processor.ProcessMessageAsync += async args => _processor.ProcessMessageAsync += ProcessReceivedMessageAsync;
{ _processor.ProcessErrorAsync += ProcessErrorAsync;
try
{
using var jsonDocument = JsonDocument.Parse(Encoding.UTF8.GetString(args.Message.Body));
var root = jsonDocument.RootElement;
if (root.ValueKind == JsonValueKind.Array) await _processor.StartProcessingAsync(cancellationToken);
{
var eventMessages = root.Deserialize<IEnumerable<EventMessage>>();
await _handler.HandleManyEventsAsync(eventMessages);
} }
else if (root.ValueKind == JsonValueKind.Object)
{
var eventMessage = root.Deserialize<EventMessage>();
await _handler.HandleEventAsync(eventMessage);
} internal Task ProcessErrorAsync(ProcessErrorEventArgs args)
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( _logger.LogError(
args.Exception, args.Exception,
@ -65,9 +42,12 @@ public class AzureServiceBusEventListenerService : EventLoggingListenerService
args.ErrorSource args.ErrorSource
); );
return Task.CompletedTask; return Task.CompletedTask;
}; }
await _processor.StartProcessingAsync(cancellationToken); 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) public override async Task StopAsync(CancellationToken cancellationToken)
@ -79,7 +59,6 @@ public class AzureServiceBusEventListenerService : EventLoggingListenerService
public override void Dispose() public override void Dispose()
{ {
_processor.DisposeAsync().GetAwaiter().GetResult(); _processor.DisposeAsync().GetAwaiter().GetResult();
_client.DisposeAsync().GetAwaiter().GetResult();
base.Dispose(); base.Dispose();
} }
} }

View File

@ -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<IEvent> 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();
}
}

View File

@ -1,7 +1,6 @@
#nullable enable #nullable enable
using Azure.Messaging.ServiceBus; using Azure.Messaging.ServiceBus;
using Bit.Core.Settings;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -10,39 +9,30 @@ namespace Bit.Core.Services;
public class AzureServiceBusIntegrationListenerService : BackgroundService public class AzureServiceBusIntegrationListenerService : BackgroundService
{ {
private readonly int _maxRetries; private readonly int _maxRetries;
private readonly string _subscriptionName; private readonly IAzureServiceBusService _serviceBusService;
private readonly string _topicName;
private readonly IIntegrationHandler _handler; private readonly IIntegrationHandler _handler;
private readonly ServiceBusClient _client;
private readonly ServiceBusProcessor _processor; private readonly ServiceBusProcessor _processor;
private readonly ServiceBusSender _sender;
private readonly ILogger<AzureServiceBusIntegrationListenerService> _logger; private readonly ILogger<AzureServiceBusIntegrationListenerService> _logger;
public AzureServiceBusIntegrationListenerService( public AzureServiceBusIntegrationListenerService(IIntegrationHandler handler,
IIntegrationHandler handler, string topicName,
string subscriptionName, string subscriptionName,
GlobalSettings globalSettings, int maxRetries,
IAzureServiceBusService serviceBusService,
ILogger<AzureServiceBusIntegrationListenerService> logger) ILogger<AzureServiceBusIntegrationListenerService> logger)
{ {
_handler = handler; _handler = handler;
_logger = logger; _logger = logger;
_maxRetries = globalSettings.EventLogging.AzureServiceBus.MaxRetries; _maxRetries = maxRetries;
_topicName = globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName; _serviceBusService = serviceBusService;
_subscriptionName = subscriptionName;
_client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); _processor = _serviceBusService.CreateProcessor(topicName, subscriptionName, new ServiceBusProcessorOptions());
_processor = _client.CreateProcessor(_topicName, _subscriptionName, new ServiceBusProcessorOptions());
_sender = _client.CreateSender(_topicName);
} }
protected override async Task ExecuteAsync(CancellationToken cancellationToken) protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{ {
_processor.ProcessMessageAsync += HandleMessageAsync; _processor.ProcessMessageAsync += HandleMessageAsync;
_processor.ProcessErrorAsync += args => _processor.ProcessErrorAsync += ProcessErrorAsync;
{
_logger.LogError(args.Exception, "Azure Service Bus error");
return Task.CompletedTask;
};
await _processor.StartProcessingAsync(cancellationToken); await _processor.StartProcessingAsync(cancellationToken);
} }
@ -51,51 +41,67 @@ public class AzureServiceBusIntegrationListenerService : BackgroundService
{ {
await _processor.StopProcessingAsync(cancellationToken); await _processor.StopProcessingAsync(cancellationToken);
await _processor.DisposeAsync(); await _processor.DisposeAsync();
await _sender.DisposeAsync();
await _client.DisposeAsync();
await base.StopAsync(cancellationToken); 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<bool> HandleMessageAsync(string body)
{
try try
{ {
var result = await _handler.HandleAsync(json); var result = await _handler.HandleAsync(body);
var message = result.Message; var message = result.Message;
if (result.Success) if (result.Success)
{ {
await args.CompleteMessageAsync(args.Message); // Successful integration. Return true to indicate the message has been handled
return; return true;
} }
message.ApplyRetry(result.DelayUntilDate); message.ApplyRetry(result.DelayUntilDate);
if (result.Retryable && message.RetryCount < _maxRetries) if (result.Retryable && message.RetryCount < _maxRetries)
{ {
var scheduledTime = (DateTime)message.DelayUntilDate!; // Publish message to the retry queue. It will be re-published for retry after a delay
var retryMsg = new ServiceBusMessage(message.ToJson()) // Return true to indicate the message has been handled
await _serviceBusService.PublishToRetryAsync(message);
return true;
}
else
{ {
Subject = args.Message.Subject, // Non-recoverable failure or exceeded the max number of retries
ScheduledEnqueueTime = scheduledTime // Return false to indicate this message should be dead-lettered
}; return false;
}
}
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;
}
}
await _sender.SendMessageAsync(retryMsg); private async Task HandleMessageAsync(ProcessMessageEventArgs args)
{
var json = args.Message.Body.ToString();
if (await HandleMessageAsync(json))
{
await args.CompleteMessageAsync(args.Message);
} }
else else
{ {
await args.DeadLetterMessageAsync(args.Message, "Retry limit exceeded or non-retryable"); await args.DeadLetterMessageAsync(args.Message, "Retry limit exceeded or non-retryable");
return;
}
await args.CompleteMessageAsync(args.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled error processing ASB message");
await args.CompleteMessageAsync(args.Message);
} }
} }
} }

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -1,4 +1,6 @@
using Bit.Core.Models.Data; #nullable enable
using Bit.Core.Models.Data;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.Services; namespace Bit.Core.Services;

View File

@ -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<IEvent> events)
{
var body = JsonSerializer.Serialize(events);
await _eventIntegrationPublisher.PublishEventAsync(body: body);
}
public async ValueTask DisposeAsync()
{
await _eventIntegrationPublisher.DisposeAsync();
}
}

View File

@ -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.Models.Data.Integrations;
using Bit.Core.AdminConsole.Utilities; using Bit.Core.AdminConsole.Utilities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -7,11 +9,9 @@ using Bit.Core.Repositories;
namespace Bit.Core.Services; namespace Bit.Core.Services;
#nullable enable
public class EventIntegrationHandler<T>( public class EventIntegrationHandler<T>(
IntegrationType integrationType, IntegrationType integrationType,
IIntegrationPublisher integrationPublisher, IEventIntegrationPublisher eventIntegrationPublisher,
IOrganizationIntegrationConfigurationRepository configurationRepository, IOrganizationIntegrationConfigurationRepository configurationRepository,
IUserRepository userRepository, IUserRepository userRepository,
IOrganizationRepository organizationRepository) IOrganizationRepository organizationRepository)
@ -34,6 +34,7 @@ public class EventIntegrationHandler<T>(
var template = configuration.Template ?? string.Empty; var template = configuration.Template ?? string.Empty;
var context = await BuildContextAsync(eventMessage, template); var context = await BuildContextAsync(eventMessage, template);
var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(template, context); var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(template, context);
var messageId = eventMessage.IdempotencyId ?? Guid.NewGuid();
var config = configuration.MergedConfiguration.Deserialize<T>() var config = configuration.MergedConfiguration.Deserialize<T>()
?? throw new InvalidOperationException($"Failed to deserialize to {typeof(T).Name}"); ?? throw new InvalidOperationException($"Failed to deserialize to {typeof(T).Name}");
@ -41,13 +42,14 @@ public class EventIntegrationHandler<T>(
var message = new IntegrationMessage<T> var message = new IntegrationMessage<T>
{ {
IntegrationType = integrationType, IntegrationType = integrationType,
MessageId = messageId.ToString(),
Configuration = config, Configuration = config,
RenderedTemplate = renderedTemplate, RenderedTemplate = renderedTemplate,
RetryCount = 0, RetryCount = 0,
DelayUntilDate = null DelayUntilDate = null
}; };
await integrationPublisher.PublishAsync(message); await eventIntegrationPublisher.PublishAsync(message);
} }
} }

View File

@ -1,4 +1,6 @@
using Bit.Core.Models.Data; #nullable enable
using Bit.Core.Models.Data;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.Services; namespace Bit.Core.Services;

View File

@ -1,4 +1,6 @@
using Bit.Core.Models.Data; #nullable enable
using Bit.Core.Models.Data;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.Services; namespace Bit.Core.Services;

View File

@ -462,13 +462,13 @@ public class EventService : IEventService
private bool CanUseEvents(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId) private bool CanUseEvents(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
{ {
return orgAbilities != null && orgAbilities.ContainsKey(orgId) && return orgAbilities != null && orgAbilities.TryGetValue(orgId, out var orgAbility) &&
orgAbilities[orgId].Enabled && orgAbilities[orgId].UseEvents; orgAbility.Enabled && orgAbility.UseEvents;
} }
private bool CanUseProviderEvents(IDictionary<Guid, ProviderAbility> providerAbilities, Guid providerId) private bool CanUseProviderEvents(IDictionary<Guid, ProviderAbility> providerAbilities, Guid providerId)
{ {
return providerAbilities != null && providerAbilities.ContainsKey(providerId) && return providerAbilities != null && providerAbilities.TryGetValue(providerId, out var providerAbility) &&
providerAbilities[providerId].Enabled && providerAbilities[providerId].UseEvents; providerAbility.Enabled && providerAbility.UseEvents;
} }
} }

View File

@ -29,9 +29,6 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Settings; 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.Utilities;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Stripe; using Stripe;
@ -56,7 +53,6 @@ public class OrganizationService : IOrganizationService
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly IPolicyService _policyService; private readonly IPolicyService _policyService;
private readonly ISsoUserRepository _ssoUserRepository; private readonly ISsoUserRepository _ssoUserRepository;
private readonly IReferenceEventService _referenceEventService;
private readonly IGlobalSettings _globalSettings; private readonly IGlobalSettings _globalSettings;
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
@ -88,7 +84,6 @@ public class OrganizationService : IOrganizationService
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
IPolicyService policyService, IPolicyService policyService,
ISsoUserRepository ssoUserRepository, ISsoUserRepository ssoUserRepository,
IReferenceEventService referenceEventService,
IGlobalSettings globalSettings, IGlobalSettings globalSettings,
IOrganizationApiKeyRepository organizationApiKeyRepository, IOrganizationApiKeyRepository organizationApiKeyRepository,
ICurrentContext currentContext, ICurrentContext currentContext,
@ -120,7 +115,6 @@ public class OrganizationService : IOrganizationService
_policyRepository = policyRepository; _policyRepository = policyRepository;
_policyService = policyService; _policyService = policyService;
_ssoUserRepository = ssoUserRepository; _ssoUserRepository = ssoUserRepository;
_referenceEventService = referenceEventService;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_organizationApiKeyRepository = organizationApiKeyRepository; _organizationApiKeyRepository = organizationApiKeyRepository;
_currentContext = currentContext; _currentContext = currentContext;
@ -153,11 +147,6 @@ public class OrganizationService : IOrganizationService
} }
await _paymentService.CancelSubscriptionAsync(organization, eop); await _paymentService.CancelSubscriptionAsync(organization, eop);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.CancelSubscription, organization, _currentContext)
{
EndOfPeriod = endOfPeriod,
});
} }
public async Task ReinstateSubscriptionAsync(Guid organizationId) public async Task ReinstateSubscriptionAsync(Guid organizationId)
@ -169,8 +158,6 @@ public class OrganizationService : IOrganizationService
} }
await _paymentService.ReinstateSubscriptionAsync(organization); await _paymentService.ReinstateSubscriptionAsync(organization);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.ReinstateSubscription, organization, _currentContext));
} }
public async Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb) public async Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb)
@ -190,13 +177,6 @@ public class OrganizationService : IOrganizationService
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, organization, storageAdjustmentGb, var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, organization, storageAdjustmentGb,
plan.PasswordManager.StripeStoragePlanId); plan.PasswordManager.StripeStoragePlanId);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.AdjustStorage, organization, _currentContext)
{
PlanName = plan.Name,
PlanType = plan.Type,
Storage = storageAdjustmentGb,
});
await ReplaceAndUpdateCacheAsync(organization); await ReplaceAndUpdateCacheAsync(organization);
return secret; return secret;
} }
@ -328,14 +308,6 @@ public class OrganizationService : IOrganizationService
} }
var paymentIntentClientSecret = await _paymentService.AdjustSeatsAsync(organization, plan, additionalSeats); 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; organization.Seats = (short?)newSeatTotal;
await ReplaceAndUpdateCacheAsync(organization); await ReplaceAndUpdateCacheAsync(organization);
@ -640,12 +612,12 @@ public class OrganizationService : IOrganizationService
} }
var providers = organization.GetTwoFactorProviders(); var providers = organization.GetTwoFactorProviders();
if (!providers?.ContainsKey(type) ?? true) if (providers is null || !providers.TryGetValue(type, out var provider))
{ {
return; return;
} }
providers[type].Enabled = true; provider.Enabled = true;
organization.SetTwoFactorProviders(providers); organization.SetTwoFactorProviders(providers);
await UpdateAsync(organization); await UpdateAsync(organization);
} }
@ -886,12 +858,6 @@ public class OrganizationService : IOrganizationService
} }
await SendInvitesAsync(allOrgUsers, organization); await SendInvitesAsync(allOrgUsers, organization);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.InvitedUsers, organization, _currentContext)
{
Users = orgUserInvitedCount
});
} }
catch (Exception e) catch (Exception e)
{ {
@ -1149,7 +1115,7 @@ public class OrganizationService : IOrganizationService
var existingUsersDict = existingExternalUsers.ToDictionary(u => u.ExternalId); var existingUsersDict = existingExternalUsers.ToDictionary(u => u.ExternalId);
var removeUsersSet = new HashSet<string>(removeUserExternalIds) var removeUsersSet = new HashSet<string>(removeUserExternalIds)
.Except(newUsersSet) .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]); .Select(u => existingUsersDict[u]);
await _organizationUserRepository.DeleteManyAsync(removeUsersSet.Select(u => u.Id)); 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 _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) public async Task DeleteSsoUserAsync(Guid userId, Guid? organizationId)
@ -1754,11 +1718,5 @@ public class OrganizationService : IOrganizationService
await SendInviteAsync(ownerOrganizationUser, organization, true); await SendInviteAsync(ownerOrganizationUser, organization, true);
await _eventService.LogOrganizationUserEventAsync(ownerOrganizationUser, EventType.OrganizationUser_Invited); await _eventService.LogOrganizationUserEventAsync(ownerOrganizationUser, EventType.OrganizationUser_Invited);
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.OrganizationCreatedByAdmin, organization, _currentContext)
{
EventRaisedByUser = userService.GetUserName(user),
SalesAssistedTrialStarted = salesAssistedTrialStarted,
});
} }
} }

View File

@ -68,7 +68,7 @@ public class PolicyService : IPolicyService
var excludedUserTypes = GetUserTypesExcludedFromPolicy(policyType); var excludedUserTypes = GetUserTypesExcludedFromPolicy(policyType);
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
return organizationUserPolicyDetails.Where(o => return organizationUserPolicyDetails.Where(o =>
(!orgAbilities.ContainsKey(o.OrganizationId) || orgAbilities[o.OrganizationId].UsePolicies) && (!orgAbilities.TryGetValue(o.OrganizationId, out var orgAbility) || orgAbility.UsePolicies) &&
o.PolicyEnabled && o.PolicyEnabled &&
!excludedUserTypes.Contains(o.OrganizationUserType) && !excludedUserTypes.Contains(o.OrganizationUserType) &&
o.OrganizationUserStatus >= minStatus && o.OrganizationUserStatus >= minStatus &&

View File

@ -1,7 +1,6 @@
using System.Text; #nullable enable
using System.Text.Json;
using Bit.Core.Models.Data; using System.Text;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using RabbitMQ.Client; using RabbitMQ.Client;
using RabbitMQ.Client.Events; using RabbitMQ.Client.Events;
@ -10,94 +9,60 @@ namespace Bit.Core.Services;
public class RabbitMqEventListenerService : EventLoggingListenerService public class RabbitMqEventListenerService : EventLoggingListenerService
{ {
private IChannel _channel; private readonly Lazy<Task<IChannel>> _lazyChannel;
private IConnection _connection;
private readonly string _exchangeName;
private readonly ConnectionFactory _factory;
private readonly ILogger<RabbitMqEventListenerService> _logger;
private readonly string _queueName; private readonly string _queueName;
private readonly IRabbitMqService _rabbitMqService;
public RabbitMqEventListenerService( public RabbitMqEventListenerService(
IEventMessageHandler handler, IEventMessageHandler handler,
ILogger<RabbitMqEventListenerService> logger, string queueName,
GlobalSettings globalSettings, IRabbitMqService rabbitMqService,
string queueName) : base(handler) ILogger<RabbitMqEventListenerService> 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; _logger = logger;
_queueName = queueName; _queueName = queueName;
_rabbitMqService = rabbitMqService;
_lazyChannel = new Lazy<Task<IChannel>>(() => _rabbitMqService.CreateChannelAsync());
} }
public override async Task StartAsync(CancellationToken cancellationToken) public override async Task StartAsync(CancellationToken cancellationToken)
{ {
_connection = await _factory.CreateConnectionAsync(cancellationToken); await _rabbitMqService.CreateEventQueueAsync(_queueName, 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 base.StartAsync(cancellationToken); await base.StartAsync(cancellationToken);
} }
protected override async Task ExecuteAsync(CancellationToken cancellationToken) protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{ {
var consumer = new AsyncEventingBasicConsumer(_channel); var channel = await _lazyChannel.Value;
consumer.ReceivedAsync += async (_, eventArgs) => var consumer = new AsyncEventingBasicConsumer(channel);
{ consumer.ReceivedAsync += async (_, eventArgs) => { await ProcessReceivedMessageAsync(eventArgs); };
try
{
using var jsonDocument = JsonDocument.Parse(Encoding.UTF8.GetString(eventArgs.Body.Span));
var root = jsonDocument.RootElement;
if (root.ValueKind == JsonValueKind.Array) await channel.BasicConsumeAsync(_queueName, autoAck: true, consumer: consumer, cancellationToken: cancellationToken);
{
var eventMessages = root.Deserialize<IEnumerable<EventMessage>>();
await _handler.HandleManyEventsAsync(eventMessages);
} }
else if (root.ValueKind == JsonValueKind.Object)
{
var eventMessage = root.Deserialize<EventMessage>();
await _handler.HandleEventAsync(eventMessage);
} internal async Task ProcessReceivedMessageAsync(BasicDeliverEventArgs eventArgs)
}
catch (Exception ex)
{ {
_logger.LogError(ex, "An error occurred while processing the message"); await ProcessReceivedMessageAsync(
} Encoding.UTF8.GetString(eventArgs.Body.Span),
}; eventArgs.BasicProperties.MessageId);
await _channel.BasicConsumeAsync(_queueName, autoAck: true, consumer: consumer, cancellationToken: cancellationToken);
} }
public override async Task StopAsync(CancellationToken cancellationToken) public override async Task StopAsync(CancellationToken cancellationToken)
{ {
await _channel.CloseAsync(cancellationToken); if (_lazyChannel.IsValueCreated)
await _connection.CloseAsync(cancellationToken); {
var channel = await _lazyChannel.Value;
await channel.CloseAsync(cancellationToken);
}
await base.StopAsync(cancellationToken); await base.StopAsync(cancellationToken);
} }
public override void Dispose() public override void Dispose()
{ {
_channel.Dispose(); if (_lazyChannel.IsValueCreated && _lazyChannel.Value.IsCompletedSuccessfully)
_connection.Dispose(); {
_lazyChannel.Value.Result.Dispose();
}
base.Dispose(); base.Dispose();
} }
} }

View File

@ -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<Task<IConnection>> _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<Task<IConnection>>(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<IEvent> 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<IConnection> CreateConnectionAsync()
{
return await _factory.CreateConnectionAsync();
}
}

View File

@ -1,5 +1,8 @@
using System.Text; #nullable enable
using Bit.Core.Settings;
using System.Text;
using System.Text.Json;
using Bit.Core.AdminConsole.Models.Data.Integrations;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using RabbitMQ.Client; using RabbitMQ.Client;
@ -9,97 +12,39 @@ namespace Bit.Core.Services;
public class RabbitMqIntegrationListenerService : BackgroundService 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 int _maxRetries;
private readonly string _queueName;
private readonly string _routingKey;
private readonly string _retryQueueName;
private readonly IIntegrationHandler _handler; private readonly IIntegrationHandler _handler;
private readonly ConnectionFactory _factory; private readonly Lazy<Task<IChannel>> _lazyChannel;
private readonly IRabbitMqService _rabbitMqService;
private readonly ILogger<RabbitMqIntegrationListenerService> _logger; private readonly ILogger<RabbitMqIntegrationListenerService> _logger;
private readonly int _retryTiming;
public RabbitMqIntegrationListenerService(IIntegrationHandler handler, public RabbitMqIntegrationListenerService(IIntegrationHandler handler,
string routingKey, string routingKey,
string queueName, string queueName,
string retryQueueName, string retryQueueName,
string deadLetterQueueName, int maxRetries,
GlobalSettings globalSettings, IRabbitMqService rabbitMqService,
ILogger<RabbitMqIntegrationListenerService> logger) ILogger<RabbitMqIntegrationListenerService> logger)
{ {
_handler = handler; _handler = handler;
_routingKey = routingKey; _routingKey = routingKey;
_retryRoutingKey = $"{_routingKey}-retry";
_queueName = queueName;
_retryQueueName = retryQueueName; _retryQueueName = retryQueueName;
_deadLetterQueueName = deadLetterQueueName; _queueName = queueName;
_rabbitMqService = rabbitMqService;
_logger = logger; _logger = logger;
_exchangeName = globalSettings.EventLogging.RabbitMq.IntegrationExchangeName; _maxRetries = maxRetries;
_maxRetries = globalSettings.EventLogging.RabbitMq.MaxRetries; _lazyChannel = new Lazy<Task<IChannel>>(() => _rabbitMqService.CreateChannelAsync());
_retryTiming = globalSettings.EventLogging.RabbitMq.RetryTiming;
_factory = new ConnectionFactory
{
HostName = globalSettings.EventLogging.RabbitMq.HostName,
UserName = globalSettings.EventLogging.RabbitMq.Username,
Password = globalSettings.EventLogging.RabbitMq.Password
};
} }
public override async Task StartAsync(CancellationToken cancellationToken) public override async Task StartAsync(CancellationToken cancellationToken)
{ {
_connection = await _factory.CreateConnectionAsync(cancellationToken); await _rabbitMqService.CreateIntegrationQueuesAsync(
_channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken); _queueName,
_retryQueueName,
await _channel.ExchangeDeclareAsync(exchange: _exchangeName, _routingKey,
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<string, object>
{
{ "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); cancellationToken: cancellationToken);
await base.StartAsync(cancellationToken); await base.StartAsync(cancellationToken);
@ -107,20 +52,42 @@ public class RabbitMqIntegrationListenerService : BackgroundService
protected override async Task ExecuteAsync(CancellationToken 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) => 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); 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<IntegrationMessage>(json);
if (integrationMessage is not null &&
integrationMessage.DelayUntilDate.HasValue &&
integrationMessage.DelayUntilDate.Value > DateTime.UtcNow)
{ {
await _rabbitMqService.RepublishToRetryQueueAsync(channel, ea);
await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);
return;
}
var result = await _handler.HandleAsync(json); var result = await _handler.HandleAsync(json);
var message = result.Message; var message = result.Message;
if (result.Success) if (result.Success)
{ {
// Successful integration send. Acknowledge message delivery and return // Successful integration send. Acknowledge message delivery and return
await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);
return; return;
} }
@ -132,60 +99,50 @@ public class RabbitMqIntegrationListenerService : BackgroundService
if (message.RetryCount < _maxRetries) if (message.RetryCount < _maxRetries)
{ {
// Publish message to the retry queue. It will be re-published for retry after a delay // Publish message to the retry queue. It will be re-published for retry after a delay
await _channel.BasicPublishAsync( await _rabbitMqService.PublishToRetryAsync(channel, message, cancellationToken);
exchange: _exchangeName,
routingKey: _retryRoutingKey,
body: Encoding.UTF8.GetBytes(message.ToJson()),
cancellationToken: cancellationToken);
} }
else else
{ {
// Exceeded the max number of retries; fail and send to dead letter queue // Exceeded the max number of retries; fail and send to dead letter queue
await PublishToDeadLetterAsync(message.ToJson()); await _rabbitMqService.PublishToDeadLetterAsync(channel, message, cancellationToken);
_logger.LogWarning("Max retry attempts reached. Sent to DLQ."); _logger.LogWarning("Max retry attempts reached. Sent to DLQ.");
} }
} }
else else
{ {
// Fatal error (i.e. not retryable) occurred. Send message to dead letter queue without any retries // Fatal error (i.e. not retryable) occurred. Send message to dead letter queue without any retries
await PublishToDeadLetterAsync(message.ToJson()); await _rabbitMqService.PublishToDeadLetterAsync(channel, message, cancellationToken);
_logger.LogWarning("Non-retryable failure. Sent to DLQ."); _logger.LogWarning("Non-retryable failure. Sent to DLQ.");
} }
// Message has been sent to retry or dead letter queues. // Message has been sent to retry or dead letter queues.
// Acknowledge receipt so Rabbit knows it's been processed // Acknowledge receipt so Rabbit knows it's been processed
await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);
} }
catch (Exception ex) catch (Exception ex)
{ {
// Unknown error occurred. Acknowledge so Rabbit doesn't keep attempting. Log the error // Unknown error occurred. Acknowledge so Rabbit doesn't keep attempting. Log the error
_logger.LogError(ex, "Unhandled error processing integration message."); _logger.LogError(ex, "Unhandled error processing integration message.");
await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);
} }
};
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));
} }
public override async Task StopAsync(CancellationToken cancellationToken) public override async Task StopAsync(CancellationToken cancellationToken)
{ {
await _channel.CloseAsync(cancellationToken); if (_lazyChannel.IsValueCreated)
await _connection.CloseAsync(cancellationToken); {
var channel = await _lazyChannel.Value;
await channel.CloseAsync(cancellationToken);
}
await base.StopAsync(cancellationToken); await base.StopAsync(cancellationToken);
} }
public override void Dispose() public override void Dispose()
{ {
_channel.Dispose(); if (_lazyChannel.IsValueCreated && _lazyChannel.Value.IsCompletedSuccessfully)
_connection.Dispose(); {
_lazyChannel.Value.Result.Dispose();
}
base.Dispose(); base.Dispose();
} }
} }

View File

@ -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<Task<IConnection>> _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<Task<IConnection>>(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<IConnection> CreateConnectionAsync()
{
return await _factory.CreateConnectionAsync();
}
}

View File

@ -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<Task<IConnection>> _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<Task<IConnection>>(CreateConnectionAsync);
}
public async Task<IChannel> 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<string, object?>
{
{ "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<string, object?>
{
["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<IConnection> 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<string, object?>
{
{ "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;
}
}

View File

@ -1,4 +1,6 @@
using Bit.Core.AdminConsole.Models.Data.Integrations; #nullable enable
using Bit.Core.AdminConsole.Models.Data.Integrations;
namespace Bit.Core.Services; namespace Bit.Core.Services;

View File

@ -1,4 +1,6 @@
using System.Net.Http.Headers; #nullable enable
using System.Net.Http.Headers;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Web; using System.Web;
using Bit.Core.Models.Slack; using Bit.Core.Models.Slack;
@ -22,7 +24,7 @@ public class SlackService(
public async Task<string> GetChannelIdAsync(string token, string channelName) public async Task<string> GetChannelIdAsync(string token, string channelName)
{ {
return (await GetChannelIdsAsync(token, [channelName])).FirstOrDefault(); return (await GetChannelIdsAsync(token, [channelName])).FirstOrDefault() ?? string.Empty;
} }
public async Task<List<string>> GetChannelIdsAsync(string token, List<string> channelNames) public async Task<List<string>> GetChannelIdsAsync(string token, List<string> channelNames)
@ -58,7 +60,7 @@ public class SlackService(
} }
else else
{ {
logger.LogError("Error getting Channel Ids: {Error}", result.Error); logger.LogError("Error getting Channel Ids: {Error}", result?.Error ?? "Unknown Error");
nextCursor = string.Empty; nextCursor = string.Empty;
} }
@ -89,7 +91,7 @@ public class SlackService(
new KeyValuePair<string, string>("redirect_uri", redirectUrl) new KeyValuePair<string, string>("redirect_uri", redirectUrl)
})); }));
SlackOAuthResponse result; SlackOAuthResponse? result;
try try
{ {
result = await tokenResponse.Content.ReadFromJsonAsync<SlackOAuthResponse>(); result = await tokenResponse.Content.ReadFromJsonAsync<SlackOAuthResponse>();
@ -99,7 +101,7 @@ public class SlackService(
result = null; result = null;
} }
if (result == null) if (result is null)
{ {
logger.LogError("Error obtaining token via OAuth: Unknown error"); logger.LogError("Error obtaining token via OAuth: Unknown error");
return string.Empty; return string.Empty;
@ -130,6 +132,11 @@ public class SlackService(
var response = await _httpClient.SendAsync(request); var response = await _httpClient.SendAsync(request);
var result = await response.Content.ReadFromJsonAsync<SlackUserResponse>(); var result = await response.Content.ReadFromJsonAsync<SlackUserResponse>();
if (result is null)
{
logger.LogError("Error retrieving Slack user ID: Unknown error");
return string.Empty;
}
if (!result.Ok) if (!result.Ok)
{ {
logger.LogError("Error retrieving Slack user ID: {Error}", result.Error); logger.LogError("Error retrieving Slack user ID: {Error}", result.Error);
@ -151,6 +158,11 @@ public class SlackService(
var response = await _httpClient.SendAsync(request); var response = await _httpClient.SendAsync(request);
var result = await response.Content.ReadFromJsonAsync<SlackDmResponse>(); var result = await response.Content.ReadFromJsonAsync<SlackDmResponse>();
if (result is null)
{
logger.LogError("Error opening DM channel: Unknown error");
return string.Empty;
}
if (!result.Ok) if (!result.Ok)
{ {
logger.LogError("Error opening DM channel: {Error}", result.Error); logger.LogError("Error opening DM channel: {Error}", result.Error);

View File

@ -1,4 +1,6 @@
using System.Globalization; #nullable enable
using System.Globalization;
using System.Net; using System.Net;
using System.Text; using System.Text;
using Bit.Core.AdminConsole.Models.Data.Integrations; using Bit.Core.AdminConsole.Models.Data.Integrations;
@ -29,7 +31,7 @@ public class WebhookIntegrationHandler(IHttpClientFactory httpClientFactory)
case HttpStatusCode.ServiceUnavailable: case HttpStatusCode.ServiceUnavailable:
case HttpStatusCode.GatewayTimeout: case HttpStatusCode.GatewayTimeout:
result.Retryable = true; 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)) if (response.Headers.TryGetValues("Retry-After", out var values))
{ {
@ -52,7 +54,7 @@ public class WebhookIntegrationHandler(IHttpClientFactory httpClientFactory)
break; break;
default: default:
result.Retryable = false; result.Retryable = false;
result.FailureReason = response.ReasonPhrase; result.FailureReason = response.ReasonPhrase ?? $"Failure with status code {(int)response.StatusCode}";
break; break;
} }

View File

@ -43,7 +43,7 @@ public class EmailTwoFactorTokenProvider : EmailTokenProvider
private static bool HasProperMetaData(TwoFactorProvider provider) private static bool HasProperMetaData(TwoFactorProvider provider)
{ {
return provider?.MetaData != null && provider.MetaData.ContainsKey("Email") && return provider?.MetaData != null && provider.MetaData.TryGetValue("Email", out var emailValue) &&
!string.IsNullOrWhiteSpace((string)provider.MetaData["Email"]); !string.IsNullOrWhiteSpace((string)emailValue);
} }
} }

View File

@ -80,7 +80,7 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
var keys = LoadKeys(provider); var keys = LoadKeys(provider);
if (!provider.MetaData.TryGetValue("login", out var value)) if (!provider.MetaData.TryGetValue("login", out var login))
{ {
return false; return false;
} }
@ -88,7 +88,7 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
var clientResponse = JsonSerializer.Deserialize<AuthenticatorAssertionRawResponse>(token, var clientResponse = JsonSerializer.Deserialize<AuthenticatorAssertionRawResponse>(token,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
var jsonOptions = value.ToString(); var jsonOptions = login.ToString();
var options = AssertionOptions.FromJson(jsonOptions); var options = AssertionOptions.FromJson(jsonOptions);
var webAuthCred = keys.Find(k => k.Item2.Descriptor.Id.SequenceEqual(clientResponse.Id)); var webAuthCred = keys.Find(k => k.Item2.Descriptor.Id.SequenceEqual(clientResponse.Id));
@ -148,9 +148,9 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
for (var i = 1; i <= 5; i++) for (var i = 1; i <= 5; i++)
{ {
var keyName = $"Key{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<string, TwoFactorProvider.WebAuthnData>(keyName, key)); keys.Add(new Tuple<string, TwoFactorProvider.WebAuthnData>(keyName, key));
} }

View File

@ -3,7 +3,6 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models; using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
@ -11,9 +10,6 @@ using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tokens; 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.Utilities;
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
@ -26,15 +22,12 @@ public class RegisterUserCommand : IRegisterUserCommand
private readonly IGlobalSettings _globalSettings; private readonly IGlobalSettings _globalSettings;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly IReferenceEventService _referenceEventService;
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory; private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory; private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;
private readonly IDataProtector _organizationServiceDataProtector; private readonly IDataProtector _organizationServiceDataProtector;
private readonly IDataProtector _providerServiceDataProtector; private readonly IDataProtector _providerServiceDataProtector;
private readonly ICurrentContext _currentContext;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IMailService _mailService; private readonly IMailService _mailService;
@ -48,11 +41,9 @@ public class RegisterUserCommand : IRegisterUserCommand
IGlobalSettings globalSettings, IGlobalSettings globalSettings,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
IReferenceEventService referenceEventService,
IDataProtectionProvider dataProtectionProvider, IDataProtectionProvider dataProtectionProvider,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory, IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> registrationEmailVerificationTokenDataFactory, IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> registrationEmailVerificationTokenDataFactory,
ICurrentContext currentContext,
IUserService userService, IUserService userService,
IMailService mailService, IMailService mailService,
IValidateRedemptionTokenCommand validateRedemptionTokenCommand, IValidateRedemptionTokenCommand validateRedemptionTokenCommand,
@ -62,14 +53,12 @@ public class RegisterUserCommand : IRegisterUserCommand
_globalSettings = globalSettings; _globalSettings = globalSettings;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_policyRepository = policyRepository; _policyRepository = policyRepository;
_referenceEventService = referenceEventService;
_organizationServiceDataProtector = dataProtectionProvider.CreateProtector( _organizationServiceDataProtector = dataProtectionProvider.CreateProtector(
"OrganizationServiceDataProtector"); "OrganizationServiceDataProtector");
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
_registrationEmailVerificationTokenDataFactory = registrationEmailVerificationTokenDataFactory; _registrationEmailVerificationTokenDataFactory = registrationEmailVerificationTokenDataFactory;
_currentContext = currentContext;
_userService = userService; _userService = userService;
_mailService = mailService; _mailService = mailService;
@ -86,7 +75,6 @@ public class RegisterUserCommand : IRegisterUserCommand
if (result == IdentityResult.Success) if (result == IdentityResult.Success)
{ {
await _mailService.SendWelcomeEmailAsync(user); await _mailService.SendWelcomeEmailAsync(user);
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext));
} }
return result; return result;
@ -119,12 +107,6 @@ public class RegisterUserCommand : IRegisterUserCommand
sentWelcomeEmail = true; sentWelcomeEmail = true;
if (!string.IsNullOrEmpty(initiationPath)) if (!string.IsNullOrEmpty(initiationPath))
{ {
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext)
{
SignupInitiationPath = initiationPath
});
return result; return result;
} }
} }
@ -134,8 +116,6 @@ public class RegisterUserCommand : IRegisterUserCommand
{ {
await _mailService.SendWelcomeEmailAsync(user); await _mailService.SendWelcomeEmailAsync(user);
} }
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext));
} }
return result; return result;
@ -263,10 +243,6 @@ public class RegisterUserCommand : IRegisterUserCommand
if (result == IdentityResult.Success) if (result == IdentityResult.Success)
{ {
await _mailService.SendWelcomeEmailAsync(user); await _mailService.SendWelcomeEmailAsync(user);
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext)
{
ReceiveMarketingEmails = tokenable.ReceiveMarketingEmails
});
} }
return result; return result;
@ -285,7 +261,6 @@ public class RegisterUserCommand : IRegisterUserCommand
if (result == IdentityResult.Success) if (result == IdentityResult.Success)
{ {
await _mailService.SendWelcomeEmailAsync(user); await _mailService.SendWelcomeEmailAsync(user);
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext));
} }
return result; return result;
@ -306,7 +281,6 @@ public class RegisterUserCommand : IRegisterUserCommand
if (result == IdentityResult.Success) if (result == IdentityResult.Success)
{ {
await _mailService.SendWelcomeEmailAsync(user); await _mailService.SendWelcomeEmailAsync(user);
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext));
} }
return result; return result;
@ -325,7 +299,6 @@ public class RegisterUserCommand : IRegisterUserCommand
if (result == IdentityResult.Success) if (result == IdentityResult.Success)
{ {
await _mailService.SendWelcomeEmailAsync(user); await _mailService.SendWelcomeEmailAsync(user);
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext));
} }
return result; return result;

View File

@ -23,6 +23,7 @@ public static class Constants
public const string Fido2KeyCipherMinimumVersion = "2023.10.0"; public const string Fido2KeyCipherMinimumVersion = "2023.10.0";
public const string SSHKeyCipherMinimumVersion = "2024.12.0"; public const string SSHKeyCipherMinimumVersion = "2024.12.0";
public const string DenyLegacyUserMinimumVersion = "2025.6.0";
/// <summary> /// <summary>
/// Used by IdentityServer to identify our own provider. /// 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 PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships";
public const string UsePricingService = "use-pricing-service"; public const string UsePricingService = "use-pricing-service";
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features"; 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 PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates";
public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically"; public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically";
public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup"; public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup";

View File

@ -64,39 +64,39 @@ public class CurrentContext : ICurrentContext
HttpContext = httpContext; HttpContext = httpContext;
await BuildAsync(httpContext.User, globalSettings); 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") && if (httpContext.Request.Headers.TryGetValue("Device-Type", out var deviceType) &&
Enum.TryParse(httpContext.Request.Headers["Device-Type"].ToString(), out DeviceType dType)) Enum.TryParse(deviceType.ToString(), out DeviceType dType))
{ {
DeviceType = dType; DeviceType = dType;
} }
if (!BotScore.HasValue && httpContext.Request.Headers.ContainsKey("X-Cf-Bot-Score") && if (!BotScore.HasValue && httpContext.Request.Headers.TryGetValue("X-Cf-Bot-Score", out var cfBotScore) &&
int.TryParse(httpContext.Request.Headers["X-Cf-Bot-Score"], out var parsedBotScore)) int.TryParse(cfBotScore, out var parsedBotScore))
{ {
BotScore = 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; ClientVersion = cVersion;
} }
@ -190,14 +190,14 @@ public class CurrentContext : ICurrentContext
private List<CurrentContextOrganization> GetOrganizations(Dictionary<string, IEnumerable<Claim>> claimsDict, bool orgApi) private List<CurrentContextOrganization> GetOrganizations(Dictionary<string, IEnumerable<Claim>> claimsDict, bool orgApi)
{ {
var accessSecretsManager = claimsDict.ContainsKey(Claims.SecretsManagerAccess) var accessSecretsManager = claimsDict.TryGetValue(Claims.SecretsManagerAccess, out var secretsManagerAccessClaim)
? claimsDict[Claims.SecretsManagerAccess].ToDictionary(s => s.Value, _ => true) ? secretsManagerAccessClaim.ToDictionary(s => s.Value, _ => true)
: new Dictionary<string, bool>(); : new Dictionary<string, bool>();
var organizations = new List<CurrentContextOrganization>(); var organizations = new List<CurrentContextOrganization>();
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 new CurrentContextOrganization
{ {
Id = new Guid(c.Value), 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 new CurrentContextOrganization
{ {
Id = new Guid(c.Value), 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 new CurrentContextOrganization
{ {
Id = new Guid(c.Value), 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 new CurrentContextOrganization
{ {
Id = new Guid(c.Value), Id = new Guid(c.Value),
@ -254,9 +254,9 @@ public class CurrentContext : ICurrentContext
private List<CurrentContextProvider> GetProviders(Dictionary<string, IEnumerable<Claim>> claimsDict) private List<CurrentContextProvider> GetProviders(Dictionary<string, IEnumerable<Claim>> claimsDict)
{ {
var providers = new List<CurrentContextProvider>(); var providers = new List<CurrentContextProvider>();
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 new CurrentContextProvider
{ {
Id = new Guid(c.Value), 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 new CurrentContextProvider
{ {
Id = new Guid(c.Value), Id = new Guid(c.Value),
@ -504,20 +504,20 @@ public class CurrentContext : ICurrentContext
private string GetClaimValue(Dictionary<string, IEnumerable<Claim>> claims, string type) private string GetClaimValue(Dictionary<string, IEnumerable<Claim>> claims, string type)
{ {
if (!claims.ContainsKey(type)) if (!claims.TryGetValue(type, out var claim))
{ {
return null; return null;
} }
return claims[type].FirstOrDefault()?.Value; return claim.FirstOrDefault()?.Value;
} }
private Permissions SetOrganizationPermissionsFromClaims(string organizationId, Dictionary<string, IEnumerable<Claim>> claimsDict) private Permissions SetOrganizationPermissionsFromClaims(string organizationId, Dictionary<string, IEnumerable<Claim>> claimsDict)
{ {
bool hasClaim(string claimKey) bool hasClaim(string claimKey)
{ {
return claimsDict.ContainsKey(claimKey) ? return claimsDict.TryGetValue(claimKey, out var claim) ?
claimsDict[claimKey].Any(x => x.Value == organizationId) : false; claim.Any(x => x.Value == organizationId) : false;
} }
return new Permissions return new Permissions

View File

@ -3,7 +3,6 @@ using System.Text.Json;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models; using Bit.Core.Auth.Models;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Tools.Entities;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
@ -11,7 +10,7 @@ using Microsoft.AspNetCore.Identity;
namespace Bit.Core.Entities; namespace Bit.Core.Entities;
public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFactorProvidersUser, IReferenceable public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFactorProvidersUser
{ {
private Dictionary<TwoFactorProviderType, TwoFactorProvider>? _twoFactorProviders; private Dictionary<TwoFactorProviderType, TwoFactorProvider>? _twoFactorProviders;
@ -196,12 +195,7 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider) public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider)
{ {
var providers = GetTwoFactorProviders(); var providers = GetTwoFactorProviders();
if (providers == null || !providers.TryGetValue(provider, out var value)) return providers?.GetValueOrDefault(provider);
{
return null;
}
return value;
} }
public long StorageBytesRemaining() public long StorageBytesRemaining()

View File

@ -63,6 +63,6 @@ public class DistributedCacheCookieManager : ICookieManager
private string GetKey(string key, string id) => $"{CacheKeyPrefix}-{key}-{id}"; private string GetKey(string key, string id) => $"{CacheKeyPrefix}-{key}-{id}";
private string GetId(HttpContext context, string key) => private string GetId(HttpContext context, string key) =>
context.Request.Cookies.ContainsKey(key) ? context.Request.Cookies.TryGetValue(key, out var cookie) ?
context.Request.Cookies[key] : null; cookie : null;
} }

View File

@ -8,7 +8,6 @@ using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
@ -16,9 +15,6 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services; 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; namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
@ -30,9 +26,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
private readonly IPaymentService _paymentService; private readonly IPaymentService _paymentService;
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly ISsoConfigRepository _ssoConfigRepository; private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly IReferenceEventService _referenceEventService;
private readonly IOrganizationConnectionRepository _organizationConnectionRepository; private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
private readonly ICurrentContext _currentContext;
private readonly IServiceAccountRepository _serviceAccountRepository; private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationService _organizationService; private readonly IOrganizationService _organizationService;
@ -47,9 +41,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
IPaymentService paymentService, IPaymentService paymentService,
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
ISsoConfigRepository ssoConfigRepository, ISsoConfigRepository ssoConfigRepository,
IReferenceEventService referenceEventService,
IOrganizationConnectionRepository organizationConnectionRepository, IOrganizationConnectionRepository organizationConnectionRepository,
ICurrentContext currentContext,
IServiceAccountRepository serviceAccountRepository, IServiceAccountRepository serviceAccountRepository,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationService organizationService, IOrganizationService organizationService,
@ -63,9 +55,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
_paymentService = paymentService; _paymentService = paymentService;
_policyRepository = policyRepository; _policyRepository = policyRepository;
_ssoConfigRepository = ssoConfigRepository; _ssoConfigRepository = ssoConfigRepository;
_referenceEventService = referenceEventService;
_organizationConnectionRepository = organizationConnectionRepository; _organizationConnectionRepository = organizationConnectionRepository;
_currentContext = currentContext;
_serviceAccountRepository = serviceAccountRepository; _serviceAccountRepository = serviceAccountRepository;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationService = organizationService; _organizationService = organizationService;
@ -285,25 +275,6 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
} }
await _organizationService.ReplaceAndUpdateCacheAsync(organization); 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<bool, string>(success, paymentIntentClientSecret); return new Tuple<bool, string>(success, paymentIntentClientSecret);
} }

View File

@ -1,13 +1,9 @@
#nullable enable #nullable enable
using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
namespace Bit.Core.Services; namespace Bit.Core.Services;
@ -17,23 +13,17 @@ public class CollectionService : ICollectionService
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly ICollectionRepository _collectionRepository; private readonly ICollectionRepository _collectionRepository;
private readonly IReferenceEventService _referenceEventService;
private readonly ICurrentContext _currentContext;
public CollectionService( public CollectionService(
IEventService eventService, IEventService eventService,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
ICollectionRepository collectionRepository, ICollectionRepository collectionRepository)
IReferenceEventService referenceEventService,
ICurrentContext currentContext)
{ {
_eventService = eventService; _eventService = eventService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_collectionRepository = collectionRepository; _collectionRepository = collectionRepository;
_referenceEventService = referenceEventService;
_currentContext = currentContext;
} }
public async Task SaveAsync(Collection collection, IEnumerable<CollectionAccessSelection>? groups = null, public async Task SaveAsync(Collection collection, IEnumerable<CollectionAccessSelection>? groups = null,
@ -78,7 +68,6 @@ public class CollectionService : ICollectionService
await _collectionRepository.CreateAsync(collection, org.UseGroups ? groupsList : null, usersList); await _collectionRepository.CreateAsync(collection, org.UseGroups ? groupsList : null, usersList);
await _eventService.LogCollectionEventAsync(collection, Enums.EventType.Collection_Created); await _eventService.LogCollectionEventAsync(collection, Enums.EventType.Collection_Created);
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.CollectionCreated, org, _currentContext));
} }
else else
{ {

View File

@ -50,47 +50,27 @@ public class InMemoryApplicationCacheService : IApplicationCacheService
await InitProviderAbilitiesAsync(); await InitProviderAbilitiesAsync();
var newAbility = new ProviderAbility(provider); var newAbility = new ProviderAbility(provider);
if (_providerAbilities.ContainsKey(provider.Id))
{
_providerAbilities[provider.Id] = newAbility; _providerAbilities[provider.Id] = newAbility;
} }
else
{
_providerAbilities.Add(provider.Id, newAbility);
}
}
public virtual async Task UpsertOrganizationAbilityAsync(Organization organization) public virtual async Task UpsertOrganizationAbilityAsync(Organization organization)
{ {
await InitOrganizationAbilitiesAsync(); await InitOrganizationAbilitiesAsync();
var newAbility = new OrganizationAbility(organization); var newAbility = new OrganizationAbility(organization);
if (_orgAbilities.ContainsKey(organization.Id))
{
_orgAbilities[organization.Id] = newAbility; _orgAbilities[organization.Id] = newAbility;
} }
else
{
_orgAbilities.Add(organization.Id, newAbility);
}
}
public virtual Task DeleteOrganizationAbilityAsync(Guid organizationId) public virtual Task DeleteOrganizationAbilityAsync(Guid organizationId)
{ {
if (_orgAbilities != null && _orgAbilities.ContainsKey(organizationId)) _orgAbilities?.Remove(organizationId);
{
_orgAbilities.Remove(organizationId);
}
return Task.FromResult(0); return Task.FromResult(0);
} }
public virtual Task DeleteProviderAbilityAsync(Guid providerId) public virtual Task DeleteProviderAbilityAsync(Guid providerId)
{ {
if (_providerAbilities != null && _providerAbilities.ContainsKey(providerId)) _providerAbilities?.Remove(providerId);
{
_providerAbilities.Remove(providerId);
}
return Task.FromResult(0); return Task.FromResult(0);
} }

View File

@ -182,9 +182,8 @@ public class LicensingService : ILicensingService
// Only check once per day // Only check once per day
var now = DateTime.UtcNow; 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)) if (lastCheck < now && now - lastCheck < TimeSpan.FromDays(1))
{ {
return user.Premium; return user.Premium;

View File

@ -72,8 +72,8 @@ public class SendGridMailDeliveryService : IMailDeliveryService, IDisposable
msg.SetOpenTracking(false); msg.SetOpenTracking(false);
if (message.MetaData != null && if (message.MetaData != null &&
message.MetaData.ContainsKey("SendGridBypassListManagement") && message.MetaData.TryGetValue("SendGridBypassListManagement", out var sendGridBypassListManagement) &&
Convert.ToBoolean(message.MetaData["SendGridBypassListManagement"])) Convert.ToBoolean(sendGridBypassListManagement))
{ {
msg.SetBypassListManagement(true); msg.SetBypassListManagement(true);
} }

View File

@ -30,9 +30,6 @@ using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tokens; 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.Utilities;
using Bit.Core.Vault.Repositories; using Bit.Core.Vault.Repositories;
using Fido2NetLib; using Fido2NetLib;
@ -69,7 +66,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly IPolicyService _policyService; private readonly IPolicyService _policyService;
private readonly IDataProtector _organizationServiceDataProtector; private readonly IDataProtector _organizationServiceDataProtector;
private readonly IReferenceEventService _referenceEventService;
private readonly IFido2 _fido2; private readonly IFido2 _fido2;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IGlobalSettings _globalSettings; private readonly IGlobalSettings _globalSettings;
@ -109,7 +105,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
IPaymentService paymentService, IPaymentService paymentService,
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
IPolicyService policyService, IPolicyService policyService,
IReferenceEventService referenceEventService,
IFido2 fido2, IFido2 fido2,
ICurrentContext currentContext, ICurrentContext currentContext,
IGlobalSettings globalSettings, IGlobalSettings globalSettings,
@ -154,7 +149,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
_policyService = policyService; _policyService = policyService;
_organizationServiceDataProtector = dataProtectionProvider.CreateProtector( _organizationServiceDataProtector = dataProtectionProvider.CreateProtector(
"OrganizationServiceDataProtector"); "OrganizationServiceDataProtector");
_referenceEventService = referenceEventService;
_fido2 = fido2; _fido2 = fido2;
_currentContext = currentContext; _currentContext = currentContext;
_globalSettings = globalSettings; _globalSettings = globalSettings;
@ -299,8 +293,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
} }
await _userRepository.DeleteAsync(user); await _userRepository.DeleteAsync(user);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.DeleteAccount, user, _currentContext));
await _pushService.PushLogOutAsync(user.Id); await _pushService.PushLogOutAsync(user.Id);
return IdentityResult.Success; return IdentityResult.Success;
} }
@ -365,12 +357,12 @@ public class UserService : UserManager<User>, IUserService, IDisposable
public async Task SendTwoFactorEmailAsync(User user, bool authentication = true) public async Task SendTwoFactorEmailAsync(User user, bool authentication = true)
{ {
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); 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."); throw new ArgumentNullException("No email.");
} }
var email = ((string)provider.MetaData["Email"]).ToLowerInvariant(); var email = ((string)emailValue).ToLowerInvariant();
var token = await base.GenerateTwoFactorTokenAsync(user, var token = await base.GenerateTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email)); CoreHelpers.CustomProviderName(TwoFactorProviderType.Email));
@ -398,12 +390,12 @@ public class UserService : UserManager<User>, IUserService, IDisposable
public async Task<bool> VerifyTwoFactorEmailAsync(User user, string token) public async Task<bool> VerifyTwoFactorEmailAsync(User user, string token)
{ {
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); 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."); throw new ArgumentNullException("No email.");
} }
var email = ((string)provider.MetaData["Email"]).ToLowerInvariant(); var email = ((string)emailValue).ToLowerInvariant();
return await base.VerifyTwoFactorTokenAsync(user, return await base.VerifyTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), token); CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), token);
} }
@ -461,12 +453,12 @@ public class UserService : UserManager<User>, IUserService, IDisposable
var keyId = $"Key{id}"; var keyId = $"Key{id}";
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); 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; 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 // Callback to ensure credential ID is unique. Always return true since we don't care if another
// account uses the same 2FA key. // account uses the same 2FA key.
@ -1046,12 +1038,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
{ {
await SaveUserAsync(user); await SaveUserAsync(user);
await _pushService.PushSyncVaultAsync(user.Id); await _pushService.PushSyncVaultAsync(user.Id);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.UpgradePlan, user, _currentContext)
{
Storage = user.MaxStorageGb,
PlanName = PremiumPlanId,
});
} }
catch when (!_globalSettings.SelfHosted) catch when (!_globalSettings.SelfHosted)
{ {
@ -1117,12 +1103,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb, var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb,
StripeConstants.Prices.StoragePlanPersonal); StripeConstants.Prices.StoragePlanPersonal);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.AdjustStorage, user, _currentContext)
{
Storage = storageAdjustmentGb,
PlanName = StripeConstants.Prices.StoragePlanPersonal,
});
await SaveUserAsync(user); await SaveUserAsync(user);
return secret; return secret;
} }
@ -1150,18 +1130,11 @@ public class UserService : UserManager<User>, IUserService, IDisposable
eop = false; eop = false;
} }
await _paymentService.CancelSubscriptionAsync(user, eop); await _paymentService.CancelSubscriptionAsync(user, eop);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.CancelSubscription, user, _currentContext)
{
EndOfPeriod = eop
});
} }
public async Task ReinstatePremiumAsync(User user) public async Task ReinstatePremiumAsync(User user)
{ {
await _paymentService.ReinstateSubscriptionAsync(user); await _paymentService.ReinstateSubscriptionAsync(user);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.ReinstateSubscription, user, _currentContext));
} }
public async Task EnablePremiumAsync(Guid userId, DateTime? expirationDate) public async Task EnablePremiumAsync(Guid userId, DateTime? expirationDate)
@ -1380,14 +1353,14 @@ public class UserService : UserManager<User>, IUserService, IDisposable
public void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool setEnabled = true) public void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool setEnabled = true)
{ {
var providers = user.GetTwoFactorProviders(); var providers = user.GetTwoFactorProviders();
if (!providers?.ContainsKey(type) ?? true) if (providers is null || !providers.TryGetValue(type, out var provider))
{ {
return; return;
} }
if (setEnabled) if (setEnabled)
{ {
providers[type].Enabled = true; provider.Enabled = true;
} }
user.SetTwoFactorProviders(providers); user.SetTwoFactorProviders(providers);
@ -1446,17 +1419,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
await Task.WhenAll(legacyRevokeOrgUserTasks); await Task.WhenAll(legacyRevokeOrgUserTasks);
} }
public override async Task<IdentityResult> 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) public async Task RotateApiKeyAsync(User user)
{ {
user.ApiKey = CoreHelpers.SecureRandomString(30); user.ApiKey = CoreHelpers.SecureRandomString(30);

View File

@ -327,6 +327,7 @@ public class GlobalSettings : IGlobalSettings
public int MaxRetries { get; set; } = 3; public int MaxRetries { get; set; } = 3;
public int RetryTiming { get; set; } = 30000; // 30s 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 EventRepositoryQueueName { get; set; } = "events-write-queue";
public virtual string IntegrationDeadLetterQueueName { get; set; } = "integration-dead-letter-queue"; public virtual string IntegrationDeadLetterQueueName { get; set; } = "integration-dead-letter-queue";
public virtual string SlackEventsQueueName { get; set; } = "events-slack-queue"; public virtual string SlackEventsQueueName { get; set; } = "events-slack-queue";

View File

@ -1,29 +0,0 @@
#nullable enable
using Bit.Core.Tools.Models.Business;
namespace Bit.Core.Tools.Entities;
/// <summary>
/// An entity that can be referenced by a <see cref="ReferenceEvent"/>.
/// </summary>
public interface IReferenceable
{
/// <summary>
/// Identifies the entity that generated the event.
/// </summary>
Guid Id { get; set; }
/// <summary>
/// Contextual information included in the event.
/// </summary>
/// <remarks>
/// Do not store secrets in this field.
/// </remarks>
string? ReferenceData { get; set; }
/// <summary>
/// Returns <see langword="true" /> when the entity is a user.
/// Otherwise returns <see langword="false" />.
/// </summary>
bool IsUser();
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -2,16 +2,12 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.ImportFeatures.Interfaces; 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.Entities;
using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories; using Bit.Core.Vault.Repositories;
@ -27,8 +23,6 @@ public class ImportCiphersCommand : IImportCiphersCommand
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly ICollectionRepository _collectionRepository; private readonly ICollectionRepository _collectionRepository;
private readonly IReferenceEventService _referenceEventService;
private readonly ICurrentContext _currentContext;
private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
@ -40,8 +34,6 @@ public class ImportCiphersCommand : IImportCiphersCommand
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IPushNotificationService pushService, IPushNotificationService pushService,
IPolicyService policyService, IPolicyService policyService,
IReferenceEventService referenceEventService,
ICurrentContext currentContext,
IPolicyRequirementQuery policyRequirementQuery, IPolicyRequirementQuery policyRequirementQuery,
IFeatureService featureService) IFeatureService featureService)
{ {
@ -52,8 +44,6 @@ public class ImportCiphersCommand : IImportCiphersCommand
_collectionRepository = collectionRepository; _collectionRepository = collectionRepository;
_pushService = pushService; _pushService = pushService;
_policyService = policyService; _policyService = policyService;
_referenceEventService = referenceEventService;
_currentContext = currentContext;
_policyRequirementQuery = policyRequirementQuery; _policyRequirementQuery = policyRequirementQuery;
_featureService = featureService; _featureService = featureService;
} }
@ -194,12 +184,5 @@ public class ImportCiphersCommand : IImportCiphersCommand
// push // push
await _pushService.PushSyncVaultAsync(importingUserId); await _pushService.PushSyncVaultAsync(importingUserId);
if (org != null)
{
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.VaultImported, org, _currentContext));
}
} }
} }

View File

@ -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;
/// <summary>
/// Product support monitoring.
/// </summary>
/// <remarks>
/// Do not store secrets in this type.
/// </remarks>
public class ReferenceEvent
{
/// <summary>
/// Instantiates a <see cref="ReferenceEvent"/>.
/// </summary>
public ReferenceEvent() { }
/// <inheritdoc cref="ReferenceEvent()" />
/// <param name="type">Monitored event type.</param>
/// <param name="source">Entity that created the event.</param>
/// <param name="currentContext">The conditions in which the event occurred.</param>
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;
}
}
/// <summary>
/// Monitored event type.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public ReferenceEventType Type { get; set; }
/// <summary>
/// The kind of entity that created the event.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public ReferenceEventSource Source { get; set; }
/// <inheritdoc cref="IReferenceable.Id"/>
public Guid Id { get; set; }
/// <inheritdoc cref="IReferenceable.ReferenceData"/>
public string? ReferenceData { get; set; }
/// <summary>
/// Moment the event occurred.
/// </summary>
public DateTime EventDate { get; set; } = DateTime.UtcNow;
/// <summary>
/// Number of users sent invitations by an organization.
/// </summary>
/// <value>
/// Should contain a value only on <see cref="ReferenceEventType.InvitedUsers"/> events.
/// Otherwise the value should be <see langword="null"/>.
/// </value>
public int? Users { get; set; }
/// <summary>
/// Whether or not a subscription was canceled immediately or at the end of the billing period.
/// </summary>
/// <value>
/// <see langword="true"/> when a cancellation occurs immediately.
/// <see langword="false"/> when a cancellation occurs at the end of a customer's billing period.
/// Should contain a value only on <see cref="ReferenceEventType.CancelSubscription"/> events.
/// Otherwise the value should be <see langword="null"/>.
/// </value>
public bool? EndOfPeriod { get; set; }
/// <summary>
/// Branded name of the subscription.
/// </summary>
/// <value>
/// Should contain a value only for subscription management events.
/// Otherwise the value should be <see langword="null"/>.
/// </value>
public string? PlanName { get; set; }
/// <summary>
/// Identifies a subscription.
/// </summary>
/// <value>
/// Should contain a value only for subscription management events.
/// Otherwise the value should be <see langword="null"/>.
/// </value>
public PlanType? PlanType { get; set; }
/// <summary>
/// The branded name of the prior plan.
/// </summary>
/// <value>
/// Should contain a value only on <see cref="ReferenceEventType.UpgradePlan"/> events
/// initiated by organizations.
/// Otherwise the value should be <see langword="null"/>.
/// </value>
public string? OldPlanName { get; set; }
/// <summary>
/// Identifies the prior plan
/// </summary>
/// <value>
/// Should contain a value only on <see cref="ReferenceEventType.UpgradePlan"/> events
/// initiated by organizations.
/// Otherwise the value should be <see langword="null"/>.
/// </value>
public PlanType? OldPlanType { get; set; }
/// <summary>
/// Seat count when a billable action occurs. When adjusting seats, contains
/// the new seat count.
/// </summary>
/// <value>
/// Should contain a value only on <see cref="ReferenceEventType.Rebilled"/>,
/// <see cref="ReferenceEventType.AdjustSeats"/>, <see cref="ReferenceEventType.UpgradePlan"/>,
/// and <see cref="ReferenceEventType.Signup"/> events initiated by organizations.
/// Otherwise the value should be <see langword="null"/>.
/// </value>
public int? Seats { get; set; }
/// <summary>
/// Seat count when a seat adjustment occurs.
/// </summary>
/// <value>
/// Should contain a value only on <see cref="ReferenceEventType.AdjustSeats"/>
/// events initiated by organizations.
/// Otherwise the value should be <see langword="null"/>.
/// </value>
public int? PreviousSeats { get; set; }
/// <summary>
/// Qty in GB of storage. When adjusting storage, contains the adjusted
/// storage qty. Otherwise contains the total storage quantity.
/// </summary>
/// <value>
/// Should contain a value only on <see cref="ReferenceEventType.Rebilled"/>,
/// <see cref="ReferenceEventType.AdjustStorage"/>, <see cref="ReferenceEventType.UpgradePlan"/>,
/// and <see cref="ReferenceEventType.Signup"/> events.
/// Otherwise the value should be <see langword="null"/>.
/// </value>
public short? Storage { get; set; }
/// <summary>
/// The type of send created or accessed.
/// </summary>
/// <value>
/// Should contain a value only on <see cref="ReferenceEventType.SendAccessed"/>
/// and <see cref="ReferenceEventType.SendCreated"/> events.
/// Otherwise the value should be <see langword="null"/>.
/// </value>
[JsonConverter(typeof(JsonStringEnumConverter))]
public SendType? SendType { get; set; }
/// <summary>
/// Whether the send has private notes.
/// </summary>
/// <value>
/// <see langword="true"/> when the send has private notes, otherwise <see langword="false"/>.
/// Should contain a value only on <see cref="ReferenceEventType.SendAccessed"/>
/// and <see cref="ReferenceEventType.SendCreated"/> events.
/// Otherwise the value should be <see langword="null"/>.
/// </value>
public bool? SendHasNotes { get; set; }
/// <summary>
/// The send expires after its access count exceeds this value.
/// </summary>
/// <value>
/// This field only contains a value when the send has a max access count
/// and <see cref="Type"/> is <see cref="ReferenceEventType.SendAccessed"/>
/// or <see cref="ReferenceEventType.SendCreated"/> events.
/// Otherwise, the value should be <see langword="null"/>.
/// </value>
public int? MaxAccessCount { get; set; }
/// <summary>
/// Whether the created send has a password.
/// </summary>
/// <value>
/// Should contain a value only on <see cref="ReferenceEventType.SendAccessed"/>
/// and <see cref="ReferenceEventType.SendCreated"/> events.
/// Otherwise the value should be <see langword="null"/>.
/// </value>
public bool? HasPassword { get; set; }
/// <summary>
/// The administrator that performed the action.
/// </summary>
/// <value>
/// Should contain a value only on <see cref="ReferenceEventType.OrganizationCreatedByAdmin"/>
/// and <see cref="ReferenceEventType.OrganizationEditedByAdmin"/> events.
/// Otherwise the value should be <see langword="null"/>.
/// </value>
public string? EventRaisedByUser { get; set; }
/// <summary>
/// Whether or not an organization's trial period was started by a sales person.
/// </summary>
/// <value>
/// Should contain a value only on <see cref="ReferenceEventType.OrganizationCreatedByAdmin"/>
/// and <see cref="ReferenceEventType.OrganizationEditedByAdmin"/> events.
/// Otherwise the value should be <see langword="null"/>.
/// </value>
public bool? SalesAssistedTrialStarted { get; set; }
/// <summary>
/// The installation id of the application that originated the event.
/// </summary>
/// <value>
/// <see langword="null"/> when the event was not originated by an application.
/// </value>
public string? ClientId { get; set; }
/// <summary>
/// The version of the client application that originated the event.
/// </summary>
/// <value>
/// <see langword="null"/> when the event was not originated by an application.
/// </value>
public Version? ClientVersion { get; set; }
/// <summary>
/// The initiation path of a user who signed up for a paid version of Bitwarden. For example, "Trial from marketing website".
/// </summary>
/// <value>
/// This value should only be populated when the <see cref="ReferenceEventType"/> is <see cref="ReferenceEventType.Signup"/>. Otherwise,
/// the value should be <see langword="null" />.
/// </value>
public string? SignupInitiationPath { get; set; }
/// <summary>
/// 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".
/// </summary>
/// <value>
/// <see langword="null"/> when the event was not originated by an application,
/// or when a downgrade occurred.
/// </value>
public string? PlanUpgradePath { get; set; }
/// <summary>
/// Used for the <see cref="ReferenceEventType.Signup"/> event to determine if the user has opted in to marketing emails.
/// </summary>
public bool? ReceiveMarketingEmails { get; set; }
/// <summary>
/// Used for the <see cref="ReferenceEventType.SignupEmailClicked"/> event to indicate if the user
/// landed on the registration finish screen with a valid or invalid email verification token.
/// </summary>
public bool? EmailVerificationTokenValid { get; set; }
/// <summary>
/// Used for the <see cref="ReferenceEventType.SignupEmailClicked"/> event to indicate if the user
/// landed on the registration finish screen after re-clicking an already used link.
/// </summary>
public bool? UserAlreadyExists { get; set; }
}

View File

@ -1,10 +1,8 @@
using System.Text.Json; using System.Text.Json;
using Bit.Core.Context;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Tools.Entities; using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Enums; using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Models.Data;
using Bit.Core.Tools.Repositories; using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.SendFeatures.Commands.Interfaces; using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
@ -19,8 +17,6 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand
private readonly ISendFileStorageService _sendFileStorageService; private readonly ISendFileStorageService _sendFileStorageService;
private readonly IPushNotificationService _pushNotificationService; private readonly IPushNotificationService _pushNotificationService;
private readonly ISendValidationService _sendValidationService; private readonly ISendValidationService _sendValidationService;
private readonly IReferenceEventService _referenceEventService;
private readonly ICurrentContext _currentContext;
private readonly ISendCoreHelperService _sendCoreHelperService; private readonly ISendCoreHelperService _sendCoreHelperService;
public NonAnonymousSendCommand(ISendRepository sendRepository, public NonAnonymousSendCommand(ISendRepository sendRepository,
@ -28,16 +24,12 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand
IPushNotificationService pushNotificationService, IPushNotificationService pushNotificationService,
ISendAuthorizationService sendAuthorizationService, ISendAuthorizationService sendAuthorizationService,
ISendValidationService sendValidationService, ISendValidationService sendValidationService,
IReferenceEventService referenceEventService,
ICurrentContext currentContext,
ISendCoreHelperService sendCoreHelperService) ISendCoreHelperService sendCoreHelperService)
{ {
_sendRepository = sendRepository; _sendRepository = sendRepository;
_sendFileStorageService = sendFileStorageService; _sendFileStorageService = sendFileStorageService;
_pushNotificationService = pushNotificationService; _pushNotificationService = pushNotificationService;
_sendValidationService = sendValidationService; _sendValidationService = sendValidationService;
_referenceEventService = referenceEventService;
_currentContext = currentContext;
_sendCoreHelperService = sendCoreHelperService; _sendCoreHelperService = sendCoreHelperService;
} }
@ -50,18 +42,6 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand
{ {
await _sendRepository.CreateAsync(send); await _sendRepository.CreateAsync(send);
await _pushNotificationService.PushSyncSendCreateAsync(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 else
{ {

View File

@ -1,9 +1,7 @@
using Bit.Core.Context; using Bit.Core.Entities;
using Bit.Core.Entities;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Tools.Entities; using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Enums; using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Models.Data;
using Bit.Core.Tools.Repositories; using Bit.Core.Tools.Repositories;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
@ -15,21 +13,15 @@ public class SendAuthorizationService : ISendAuthorizationService
private readonly ISendRepository _sendRepository; private readonly ISendRepository _sendRepository;
private readonly IPasswordHasher<User> _passwordHasher; private readonly IPasswordHasher<User> _passwordHasher;
private readonly IPushNotificationService _pushNotificationService; private readonly IPushNotificationService _pushNotificationService;
private readonly IReferenceEventService _referenceEventService;
private readonly ICurrentContext _currentContext;
public SendAuthorizationService( public SendAuthorizationService(
ISendRepository sendRepository, ISendRepository sendRepository,
IPasswordHasher<User> passwordHasher, IPasswordHasher<User> passwordHasher,
IPushNotificationService pushNotificationService, IPushNotificationService pushNotificationService)
IReferenceEventService referenceEventService,
ICurrentContext currentContext)
{ {
_sendRepository = sendRepository; _sendRepository = sendRepository;
_passwordHasher = passwordHasher; _passwordHasher = passwordHasher;
_pushNotificationService = pushNotificationService; _pushNotificationService = pushNotificationService;
_referenceEventService = referenceEventService;
_currentContext = currentContext;
} }
public SendAccessResult SendCanBeAccessed(Send send, public SendAccessResult SendCanBeAccessed(Send send,
@ -79,18 +71,6 @@ public class SendAuthorizationService : ISendAuthorizationService
await _sendRepository.ReplaceAsync(sendToBeAccessed); await _sendRepository.ReplaceAsync(sendToBeAccessed);
await _pushNotificationService.PushSyncSendUpdateAsync(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; return accessResult;
} }

View File

@ -1,8 +0,0 @@
using Bit.Core.Tools.Models.Business;
namespace Bit.Core.Tools.Services;
public interface IReferenceEventService
{
Task RaiseEventAsync(ReferenceEvent referenceEvent);
}

View File

@ -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
}
}
}

View File

@ -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;
}
}

View File

@ -637,9 +637,9 @@ public static class CoreHelpers
return null; 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(); return httpContext.Connection?.RemoteIpAddress?.ToString();

View File

@ -46,7 +46,7 @@ public static class LoggerFactoryExtensions
{ {
return true; 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) if (eventId?.Contains(Constants.BypassFiltersEventId.ToString()) ?? false)
{ {
return true; return true;

View File

@ -24,7 +24,7 @@ public interface ICipherService
Task DeleteFolderAsync(Folder folder); Task DeleteFolderAsync(Folder folder);
Task ShareAsync(Cipher originalCipher, Cipher cipher, Guid organizationId, IEnumerable<Guid> collectionIds, Task ShareAsync(Cipher originalCipher, Cipher cipher, Guid organizationId, IEnumerable<Guid> collectionIds,
Guid userId, DateTime? lastKnownRevisionDate); Guid userId, DateTime? lastKnownRevisionDate);
Task ShareManyAsync(IEnumerable<(Cipher cipher, DateTime? lastKnownRevisionDate)> ciphers, Guid organizationId, Task<IEnumerable<CipherDetails>> ShareManyAsync(IEnumerable<(CipherDetails cipher, DateTime? lastKnownRevisionDate)> ciphers, Guid organizationId,
IEnumerable<Guid> collectionIds, Guid sharingUserId); IEnumerable<Guid> collectionIds, Guid sharingUserId);
Task SaveCollectionsAsync(Cipher cipher, IEnumerable<Guid> collectionIds, Guid savingUserId, bool orgAdmin); Task SaveCollectionsAsync(Cipher cipher, IEnumerable<Guid> collectionIds, Guid savingUserId, bool orgAdmin);
Task SoftDeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false); Task SoftDeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false);

Some files were not shown because too many files have changed in this diff Show More