From 7af50172e022b0bd31bcf0242886b2ac8077ed95 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Tue, 7 Jul 2020 12:01:34 -0400 Subject: [PATCH] Reference event service implementation (#811) * Reference event service implementation * Fix IReferenceable implementation of Id * add structure to event body --- src/Core/Enums/ReferenceEventSource.cs | 12 +++++ src/Core/Enums/ReferenceEventType.cs | 22 +++++++++ src/Core/Models/Business/ReferenceEvent.cs | 47 ++++++++++++++++++ src/Core/Models/Table/IReferenceable.cs | 11 +++++ src/Core/Models/Table/Organization.cs | 2 +- src/Core/Models/Table/User.cs | 2 +- src/Core/Services/IReferenceEventService.cs | 10 ++++ .../AzureQueueReferenceEventService.cs | 49 +++++++++++++++++++ .../Implementations/OrganizationService.cs | 48 +++++++++++++++++- .../Services/Implementations/UserService.cs | 26 ++++++++++ .../NoopReferenceEventService.cs | 13 +++++ .../Utilities/ServiceCollectionExtensions.cs | 9 ++++ 12 files changed, 248 insertions(+), 3 deletions(-) create mode 100644 src/Core/Enums/ReferenceEventSource.cs create mode 100644 src/Core/Enums/ReferenceEventType.cs create mode 100644 src/Core/Models/Business/ReferenceEvent.cs create mode 100644 src/Core/Models/Table/IReferenceable.cs create mode 100644 src/Core/Services/IReferenceEventService.cs create mode 100644 src/Core/Services/Implementations/AzureQueueReferenceEventService.cs create mode 100644 src/Core/Services/NoopImplementations/NoopReferenceEventService.cs diff --git a/src/Core/Enums/ReferenceEventSource.cs b/src/Core/Enums/ReferenceEventSource.cs new file mode 100644 index 000000000..0a19b0772 --- /dev/null +++ b/src/Core/Enums/ReferenceEventSource.cs @@ -0,0 +1,12 @@ +using System.Runtime.Serialization; + +namespace Bit.Core.Enums +{ + public enum ReferenceEventSource + { + [EnumMember(Value = "organization")] + Organization, + [EnumMember(Value = "user")] + User, + } +} diff --git a/src/Core/Enums/ReferenceEventType.cs b/src/Core/Enums/ReferenceEventType.cs new file mode 100644 index 000000000..abc7886c5 --- /dev/null +++ b/src/Core/Enums/ReferenceEventType.cs @@ -0,0 +1,22 @@ +using System.Runtime.Serialization; + +namespace Bit.Core.Enums +{ + public enum ReferenceEventType + { + [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, + } +} diff --git a/src/Core/Models/Business/ReferenceEvent.cs b/src/Core/Models/Business/ReferenceEvent.cs new file mode 100644 index 000000000..5e430e36f --- /dev/null +++ b/src/Core/Models/Business/ReferenceEvent.cs @@ -0,0 +1,47 @@ +using System; +using Bit.Core.Enums; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; + +namespace Bit.Core.Models.Business +{ + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class ReferenceEvent + { + public ReferenceEvent() { } + + public ReferenceEvent(ReferenceEventType type, IReferenceable source) + { + Type = type; + if (source != null) + { + Source = source.IsUser() ? ReferenceEventSource.User : ReferenceEventSource.Organization; + Id = source.Id; + ReferenceId = source.ReferenceId; + } + } + + [JsonConverter(typeof(StringEnumConverter))] + public ReferenceEventType Type { get; set; } + + [JsonConverter(typeof(StringEnumConverter))] + public ReferenceEventSource Source { get; set; } + + public Guid Id { get; set; } + + public string ReferenceId { get; set; } + + public DateTime EventDate { get; set; } = DateTime.UtcNow; + + public bool? EndOfPeriod { get; set; } + + public string PlanName { get; set; } + + public PlanType? PlanType { get; set; } + + public short? Seats { get; set; } + + public short? Storage { get; set; } + } +} diff --git a/src/Core/Models/Table/IReferenceable.cs b/src/Core/Models/Table/IReferenceable.cs new file mode 100644 index 000000000..e412c9b52 --- /dev/null +++ b/src/Core/Models/Table/IReferenceable.cs @@ -0,0 +1,11 @@ +using System; + +namespace Bit.Core.Models +{ + public interface IReferenceable + { + Guid Id { get; set; } + string ReferenceId { get; set; } + bool IsUser(); + } +} diff --git a/src/Core/Models/Table/Organization.cs b/src/Core/Models/Table/Organization.cs index e32e1a9b3..57843ddb5 100644 --- a/src/Core/Models/Table/Organization.cs +++ b/src/Core/Models/Table/Organization.cs @@ -7,7 +7,7 @@ using System.Linq; namespace Bit.Core.Models.Table { - public class Organization : ITableObject, ISubscriber, IStorable, IStorableSubscriber, IRevisable + public class Organization : ITableObject, ISubscriber, IStorable, IStorableSubscriber, IRevisable, IReferenceable { private Dictionary _twoFactorProviders; diff --git a/src/Core/Models/Table/User.cs b/src/Core/Models/Table/User.cs index ba766f941..e53731513 100644 --- a/src/Core/Models/Table/User.cs +++ b/src/Core/Models/Table/User.cs @@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Identity; namespace Bit.Core.Models.Table { - public class User : ITableObject, ISubscriber, IStorable, IStorableSubscriber, IRevisable, ITwoFactorProvidersUser + public class User : ITableObject, ISubscriber, IStorable, IStorableSubscriber, IRevisable, ITwoFactorProvidersUser, IReferenceable { private Dictionary _twoFactorProviders; diff --git a/src/Core/Services/IReferenceEventService.cs b/src/Core/Services/IReferenceEventService.cs new file mode 100644 index 000000000..c17fca4e6 --- /dev/null +++ b/src/Core/Services/IReferenceEventService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Bit.Core.Models.Business; + +namespace Bit.Core.Services +{ + public interface IReferenceEventService + { + Task RaiseEventAsync(ReferenceEvent referenceEvent); + } +} diff --git a/src/Core/Services/Implementations/AzureQueueReferenceEventService.cs b/src/Core/Services/Implementations/AzureQueueReferenceEventService.cs new file mode 100644 index 000000000..13d3c6626 --- /dev/null +++ b/src/Core/Services/Implementations/AzureQueueReferenceEventService.cs @@ -0,0 +1,49 @@ +using System.Threading.Tasks; +using Azure.Storage.Queues; +using Bit.Core.Models.Business; +using Newtonsoft.Json; + +namespace Bit.Core.Services +{ + public class AzureQueueReferenceEventService : IReferenceEventService + { + private const string _queueName = "reference-events"; + + private readonly QueueClient _queueClient; + private readonly GlobalSettings _globalSettings; + private readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + }; + + public AzureQueueReferenceEventService ( + GlobalSettings globalSettings) + { + _queueClient = new QueueClient(globalSettings.Storage.ConnectionString, _queueName); + _globalSettings = globalSettings; + } + + public async Task RaiseEventAsync(ReferenceEvent referenceEvent) + { + await SendMessageAsync(referenceEvent); + } + + private async Task SendMessageAsync(ReferenceEvent referenceEvent) + { + if (_globalSettings.SelfHosted || string.IsNullOrWhiteSpace(referenceEvent.ReferenceId)) + { + // Ignore for self-hosted, OR, where there is no ReferenceId + return; + } + try + { + var message = JsonConvert.SerializeObject(referenceEvent, _jsonSerializerSettings); + await _queueClient.SendMessageAsync(message); + } + catch + { + // Ignore failure + } + } + } +} diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 9d20e4da0..4f5a06b9a 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -34,6 +34,7 @@ namespace Bit.Core.Services private readonly IApplicationCacheService _applicationCacheService; private readonly IPaymentService _paymentService; private readonly IPolicyRepository _policyRepository; + private readonly IReferenceEventService _referenceEventService; private readonly GlobalSettings _globalSettings; public OrganizationService( @@ -53,6 +54,7 @@ namespace Bit.Core.Services IApplicationCacheService applicationCacheService, IPaymentService paymentService, IPolicyRepository policyRepository, + IReferenceEventService referenceEventService, GlobalSettings globalSettings) { _organizationRepository = organizationRepository; @@ -71,6 +73,7 @@ namespace Bit.Core.Services _applicationCacheService = applicationCacheService; _paymentService = paymentService; _policyRepository = policyRepository; + _referenceEventService = referenceEventService; _globalSettings = globalSettings; } @@ -108,6 +111,11 @@ namespace Bit.Core.Services } await _paymentService.CancelSubscriptionAsync(organization, eop); + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.CancelSubscription, organization) + { + EndOfPeriod = endOfPeriod, + }); } public async Task ReinstateSubscriptionAsync(Guid organizationId) @@ -119,6 +127,8 @@ namespace Bit.Core.Services } await _paymentService.ReinstateSubscriptionAsync(organization); + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.ReinstateSubscription, organization)); } public async Task> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade) @@ -240,6 +250,17 @@ namespace Bit.Core.Services organization.Plan = newPlan.Name; organization.Enabled = success; await ReplaceAndUpdateCache(organization); + if (success) + { + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.UpgradePlan, organization) + { + PlanName = newPlan.Name, + PlanType = newPlan.Type, + Seats = organization.Seats, + Storage = organization.MaxStorageGb, + }); + } return new Tuple(success, paymentIntentClientSecret); } @@ -265,6 +286,13 @@ namespace Bit.Core.Services var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, organization, storageAdjustmentGb, plan.StripeStoragePlanId); + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.AdjustStorage, organization) + { + PlanName = plan.Name, + PlanType = plan.Type, + Storage = storageAdjustmentGb, + }); await ReplaceAndUpdateCache(organization); return secret; } @@ -399,6 +427,13 @@ namespace Bit.Core.Services } organization.Seats = (short?)newSeatTotal; + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.AdjustSeats, organization) + { + PlanName = plan.Name, + PlanType = plan.Type, + Seats = organization.Seats, + }); await ReplaceAndUpdateCache(organization); return paymentIntentClientSecret; } @@ -503,7 +538,16 @@ namespace Bit.Core.Services signup.PremiumAccessAddon, signup.TaxInfo); } - return await SignUpAsync(organization, signup.Owner.Id, signup.OwnerKey, signup.CollectionName, true); + var returnValue = await SignUpAsync(organization, signup.Owner.Id, signup.OwnerKey, signup.CollectionName, true); + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.Signup, organization) + { + PlanName = plan.Name, + PlanType = plan.Type, + Seats = returnValue.Item1.Seats, + Storage = returnValue.Item1.MaxStorageGb, + }); + return returnValue; } public async Task> SignUpAsync( @@ -741,6 +785,8 @@ namespace Bit.Core.Services var eop = !organization.ExpirationDate.HasValue || organization.ExpirationDate.Value >= DateTime.UtcNow; await _paymentService.CancelSubscriptionAsync(organization, eop); + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.DeleteAccount, organization)); } catch (GatewayException) { } } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index cfbb64888..bcdf80da0 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -45,6 +45,7 @@ namespace Bit.Core.Services private readonly IPaymentService _paymentService; private readonly IPolicyRepository _policyRepository; private readonly IDataProtector _organizationServiceDataProtector; + private readonly IReferenceEventService _referenceEventService; private readonly CurrentContext _currentContext; private readonly GlobalSettings _globalSettings; @@ -71,6 +72,7 @@ namespace Bit.Core.Services IDataProtectionProvider dataProtectionProvider, IPaymentService paymentService, IPolicyRepository policyRepository, + IReferenceEventService referenceEventService, CurrentContext currentContext, GlobalSettings globalSettings) : base( @@ -102,6 +104,7 @@ namespace Bit.Core.Services _policyRepository = policyRepository; _organizationServiceDataProtector = dataProtectionProvider.CreateProtector( "OrganizationServiceDataProtector"); + _referenceEventService = referenceEventService; _currentContext = currentContext; _globalSettings = globalSettings; } @@ -219,6 +222,8 @@ namespace Bit.Core.Services } await _userRepository.DeleteAsync(user); + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.DeleteAccount, user)); await _pushService.PushLogOutAsync(user.Id); return IdentityResult.Success; } @@ -288,6 +293,8 @@ namespace Bit.Core.Services if (result == IdentityResult.Success) { await _mailService.SendWelcomeEmailAsync(user); + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.Signup, user)); } return result; @@ -765,6 +772,12 @@ namespace Bit.Core.Services { await SaveUserAsync(user); await _pushService.PushSyncVaultAsync(user.Id); + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.UpgradePlan, user) + { + Storage = user.MaxStorageGb, + PlanName = PremiumPlanId, + }); } catch when(!_globalSettings.SelfHosted) { @@ -841,6 +854,12 @@ namespace Bit.Core.Services var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb, StoragePlanId); + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.AdjustStorage, user) + { + Storage = storageAdjustmentGb, + PlanName = StoragePlanId, + }); await SaveUserAsync(user); return secret; } @@ -868,11 +887,18 @@ namespace Bit.Core.Services eop = false; } await _paymentService.CancelSubscriptionAsync(user, eop, accountDelete); + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.CancelSubscription, user) + { + EndOfPeriod = eop, + }); } public async Task ReinstatePremiumAsync(User user) { await _paymentService.ReinstateSubscriptionAsync(user); + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.ReinstateSubscription, user)); } public async Task EnablePremiumAsync(Guid userId, DateTime? expirationDate) diff --git a/src/Core/Services/NoopImplementations/NoopReferenceEventService.cs b/src/Core/Services/NoopImplementations/NoopReferenceEventService.cs new file mode 100644 index 000000000..ccc8146f5 --- /dev/null +++ b/src/Core/Services/NoopImplementations/NoopReferenceEventService.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using Bit.Core.Models.Business; + +namespace Bit.Core.Services +{ + public class NoopReferenceEventService : IReferenceEventService + { + public Task RaiseEventAsync(ReferenceEvent referenceEvent) + { + return Task.CompletedTask; + } + } +} diff --git a/src/Core/Utilities/ServiceCollectionExtensions.cs b/src/Core/Utilities/ServiceCollectionExtensions.cs index ac49a7718..0e702dc0c 100644 --- a/src/Core/Utilities/ServiceCollectionExtensions.cs +++ b/src/Core/Utilities/ServiceCollectionExtensions.cs @@ -196,6 +196,15 @@ namespace Bit.Core.Utilities { services.AddSingleton(); } + + if (globalSettings.SelfHosted) + { + services.AddSingleton(); + } + else + { + services.AddSingleton(); + } } public static void AddNoopServices(this IServiceCollection services)