From 3f629e0a5ae68681ead65a7e501d00894c902841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:21:51 +0100 Subject: [PATCH 01/29] [PM-11334] Add managed status to sync data (#4791) * Refactor UserService to add GetOrganizationManagingUserAsync method to retrive the organization that manages a user * Refactor SyncController and AccountsController to include ManagedByOrganizationId in profile response --- .../Auth/Controllers/AccountsController.cs | 35 ++++++++++++++++--- .../Models/Response/ProfileResponseModel.cs | 5 ++- src/Api/Vault/Controllers/SyncController.cs | 27 +++++++++++--- .../Models/Response/SyncResponseModel.cs | 3 +- src/Core/Services/IUserService.cs | 7 ++++ .../Services/Implementations/UserService.cs | 9 ++++- 6 files changed, 75 insertions(+), 11 deletions(-) diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 3370b8939..cf74460fc 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -443,10 +443,11 @@ public class AccountsController : Controller var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); + var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user); var response = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, twoFactorEnabled, - hasPremiumFromOrg); + hasPremiumFromOrg, managedByOrganizationId); return response; } @@ -471,7 +472,12 @@ public class AccountsController : Controller } await _userService.SaveUserAsync(model.ToUser(user)); - var response = new ProfileResponseModel(user, null, null, null, await _userService.TwoFactorIsEnabledAsync(user), await _userService.HasPremiumFromOrganization(user)); + + var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); + var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user); + + var response = new ProfileResponseModel(user, null, null, null, twoFactorEnabled, hasPremiumFromOrg, managedByOrganizationId); return response; } @@ -485,7 +491,12 @@ public class AccountsController : Controller throw new UnauthorizedAccessException(); } await _userService.SaveUserAsync(model.ToUser(user), true); - var response = new ProfileResponseModel(user, null, null, null, await _userService.TwoFactorIsEnabledAsync(user), await _userService.HasPremiumFromOrganization(user)); + + var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); + var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user); + + var response = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId); return response; } @@ -633,7 +644,12 @@ public class AccountsController : Controller BillingAddressCountry = model.Country, BillingAddressPostalCode = model.PostalCode, }); - var profile = new ProfileResponseModel(user, null, null, null, await _userService.TwoFactorIsEnabledAsync(user), await _userService.HasPremiumFromOrganization(user)); + + var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); + var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user); + + var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId); return new PaymentResponseModel { UserProfile = profile, @@ -920,4 +936,15 @@ public class AccountsController : Controller throw new BadRequestException("Token", "Invalid token"); } } + + private async Task GetManagedByOrganizationIdAsync(User user) + { + if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) + { + return null; + } + + var organizationManagingUser = await _userService.GetOrganizationManagingUserAsync(user.Id); + return organizationManagingUser?.Id; + } } diff --git a/src/Api/Models/Response/ProfileResponseModel.cs b/src/Api/Models/Response/ProfileResponseModel.cs index fbb60e718..f5d0382e5 100644 --- a/src/Api/Models/Response/ProfileResponseModel.cs +++ b/src/Api/Models/Response/ProfileResponseModel.cs @@ -14,7 +14,8 @@ public class ProfileResponseModel : ResponseModel IEnumerable providerUserDetails, IEnumerable providerUserOrganizationDetails, bool twoFactorEnabled, - bool premiumFromOrganization) : base("profile") + bool premiumFromOrganization, + Guid? managedByOrganizationId) : base("profile") { if (user == null) { @@ -40,6 +41,7 @@ public class ProfileResponseModel : ResponseModel Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p)); ProviderOrganizations = providerUserOrganizationDetails?.Select(po => new ProfileProviderOrganizationResponseModel(po)); + ManagedByOrganizationId = managedByOrganizationId; } public ProfileResponseModel() : base("profile") @@ -61,6 +63,7 @@ public class ProfileResponseModel : ResponseModel public bool UsesKeyConnector { get; set; } public string AvatarColor { get; set; } public DateTime CreationDate { get; set; } + public Guid? ManagedByOrganizationId { get; set; } public IEnumerable Organizations { get; set; } public IEnumerable Providers { get; set; } public IEnumerable ProviderOrganizations { get; set; } diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index 0381bdca6..79c71bb87 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -1,4 +1,5 @@ using Bit.Api.Vault.Models.Response; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; @@ -6,6 +7,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -30,6 +32,7 @@ public class SyncController : Controller private readonly IPolicyRepository _policyRepository; private readonly ISendRepository _sendRepository; private readonly GlobalSettings _globalSettings; + private readonly IFeatureService _featureService; public SyncController( IUserService userService, @@ -41,7 +44,8 @@ public class SyncController : Controller IProviderUserRepository providerUserRepository, IPolicyRepository policyRepository, ISendRepository sendRepository, - GlobalSettings globalSettings) + GlobalSettings globalSettings, + IFeatureService featureService) { _userService = userService; _folderRepository = folderRepository; @@ -53,6 +57,7 @@ public class SyncController : Controller _policyRepository = policyRepository; _sendRepository = sendRepository; _globalSettings = globalSettings; + _featureService = featureService; } [HttpGet("")] @@ -90,9 +95,23 @@ public class SyncController : Controller var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); - var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationUserDetails, - providerUserDetails, providerUserOrganizationDetails, folders, collections, ciphers, - collectionCiphersGroupDict, excludeDomains, policies, sends); + var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user, organizationUserDetails); + + var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization, + managedByOrganizationId, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, + folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends); return response; } + + private async Task GetManagedByOrganizationIdAsync(User user, IEnumerable organizationUserDetails) + { + if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) || + !organizationUserDetails.Any(o => o.Enabled && o.UseSso)) + { + return null; + } + + var organizationManagingUser = await _userService.GetOrganizationManagingUserAsync(user.Id); + return organizationManagingUser?.Id; + } } diff --git a/src/Api/Vault/Models/Response/SyncResponseModel.cs b/src/Api/Vault/Models/Response/SyncResponseModel.cs index ca833738a..2170a5232 100644 --- a/src/Api/Vault/Models/Response/SyncResponseModel.cs +++ b/src/Api/Vault/Models/Response/SyncResponseModel.cs @@ -21,6 +21,7 @@ public class SyncResponseModel : ResponseModel User user, bool userTwoFactorEnabled, bool userHasPremiumFromOrganization, + Guid? managedByOrganizationId, IEnumerable organizationUserDetails, IEnumerable providerUserDetails, IEnumerable providerUserOrganizationDetails, @@ -34,7 +35,7 @@ public class SyncResponseModel : ResponseModel : base("sync") { Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails, - providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization); + providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId); Folders = folders.Select(f => new FolderResponseModel(f)); Ciphers = ciphers.Select(c => new CipherDetailsResponseModel(c, globalSettings, collectionCiphersDict)); Collections = collections?.Select( diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index f3ada234a..0135b5f1b 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -1,4 +1,5 @@ using System.Security.Claims; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Entities; @@ -95,4 +96,10 @@ public interface IUserService /// The organization must be enabled and be on an Enterprise plan. /// Task IsManagedByAnyOrganizationAsync(Guid userId); + + /// + /// Gets the organization that manages the user. + /// + /// + Task GetOrganizationManagingUserAsync(Guid userId); } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 46f48ef26..87fdd75fe 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1,4 +1,5 @@ using System.Security.Claims; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; @@ -1245,13 +1246,19 @@ public class UserService : UserManager, IUserService, IDisposable } public async Task IsManagedByAnyOrganizationAsync(Guid userId) + { + var managingOrganization = await GetOrganizationManagingUserAsync(userId); + return managingOrganization != null; + } + + public async Task GetOrganizationManagingUserAsync(Guid userId) { // Users can only be managed by an Organization that is enabled and can have organization domains var organization = await _organizationRepository.GetByClaimedUserDomainAsync(userId); // TODO: Replace "UseSso" with a new organization ability like "UseOrganizationDomains" (PM-11622). // Verified domains were tied to SSO, so we currently check the "UseSso" organization ability. - return organization is { Enabled: true, UseSso: true }; + return (organization is { Enabled: true, UseSso: true }) ? organization : null; } /// From 05247d2525f2ae08f7fa6efcefe2875719509dcc Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 26 Sep 2024 09:18:21 -0400 Subject: [PATCH 02/29] [PM-12420] Stripe events recovery (#4793) * Billing: Add event recovery endpoints * Core: Add InternalBilling to BaseServiceUriSettings * Admin: Scaffold billing section * Admin: Scaffold ProcessStripeEvents section * Admin: Implement event processing * Run dotnet format --- src/Admin/Admin.csproj | 4 ++ .../ProcessStripeEventsController.cs | 71 +++++++++++++++++++ .../ProcessStripeEvents/EventsFormModel.cs | 29 ++++++++ .../ProcessStripeEvents/EventsRequestBody.cs | 9 +++ .../ProcessStripeEvents/EventsResponseBody.cs | 39 ++++++++++ .../Views/ProcessStripeEvents/Index.cshtml | 25 +++++++ .../Views/ProcessStripeEvents/Results.cshtml | 49 +++++++++++++ src/Admin/Billing/Views/_ViewImports.cshtml | 5 ++ src/Admin/Billing/Views/_ViewStart.cshtml | 3 + src/Admin/Enums/Permissions.cs | 3 +- src/Admin/Startup.cs | 2 + src/Admin/Utilities/RolePermissionMapping.cs | 3 +- src/Admin/Views/Shared/_Layout.cshtml | 7 ++ src/Admin/appsettings.Development.json | 3 +- src/Billing/Controllers/RecoveryController.cs | 68 ++++++++++++++++++ .../Models/Recovery/EventsRequestBody.cs | 9 +++ .../Models/Recovery/EventsResponseBody.cs | 31 ++++++++ src/Billing/Services/IStripeFacade.cs | 6 ++ .../Services/Implementations/StripeFacade.cs | 8 +++ src/Core/Settings/GlobalSettings.cs | 7 ++ src/Core/Settings/IBaseServiceUriSettings.cs | 1 + 21 files changed, 379 insertions(+), 3 deletions(-) create mode 100644 src/Admin/Billing/Controllers/ProcessStripeEventsController.cs create mode 100644 src/Admin/Billing/Models/ProcessStripeEvents/EventsFormModel.cs create mode 100644 src/Admin/Billing/Models/ProcessStripeEvents/EventsRequestBody.cs create mode 100644 src/Admin/Billing/Models/ProcessStripeEvents/EventsResponseBody.cs create mode 100644 src/Admin/Billing/Views/ProcessStripeEvents/Index.cshtml create mode 100644 src/Admin/Billing/Views/ProcessStripeEvents/Results.cshtml create mode 100644 src/Admin/Billing/Views/_ViewImports.cshtml create mode 100644 src/Admin/Billing/Views/_ViewStart.cshtml create mode 100644 src/Billing/Controllers/RecoveryController.cs create mode 100644 src/Billing/Models/Recovery/EventsRequestBody.cs create mode 100644 src/Billing/Models/Recovery/EventsResponseBody.cs diff --git a/src/Admin/Admin.csproj b/src/Admin/Admin.csproj index cd30e841b..5493e65af 100644 --- a/src/Admin/Admin.csproj +++ b/src/Admin/Admin.csproj @@ -14,6 +14,10 @@ + + + + diff --git a/src/Admin/Billing/Controllers/ProcessStripeEventsController.cs b/src/Admin/Billing/Controllers/ProcessStripeEventsController.cs new file mode 100644 index 000000000..1a3f56a18 --- /dev/null +++ b/src/Admin/Billing/Controllers/ProcessStripeEventsController.cs @@ -0,0 +1,71 @@ +using System.Text.Json; +using Bit.Admin.Billing.Models.ProcessStripeEvents; +using Bit.Core.Settings; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Admin.Billing.Controllers; + +[Authorize] +[Route("process-stripe-events")] +[SelfHosted(NotSelfHostedOnly = true)] +public class ProcessStripeEventsController( + IHttpClientFactory httpClientFactory, + IGlobalSettings globalSettings) : Controller +{ + [HttpGet] + public ActionResult Index() + { + return View(new EventsFormModel()); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task ProcessAsync([FromForm] EventsFormModel model) + { + var eventIds = model.GetEventIds(); + + const string baseEndpoint = "stripe/recovery/events"; + + var endpoint = model.Inspect ? $"{baseEndpoint}/inspect" : $"{baseEndpoint}/process"; + + var (response, failedResponseMessage) = await PostAsync(endpoint, new EventsRequestBody + { + EventIds = eventIds + }); + + if (response == null) + { + return StatusCode((int)failedResponseMessage.StatusCode, "An error occurred during your request."); + } + + response.ActionType = model.Inspect ? EventActionType.Inspect : EventActionType.Process; + + return View("Results", response); + } + + private async Task<(EventsResponseBody, HttpResponseMessage)> PostAsync( + string endpoint, + EventsRequestBody requestModel) + { + var client = httpClientFactory.CreateClient("InternalBilling"); + client.BaseAddress = new Uri(globalSettings.BaseServiceUri.InternalBilling); + + var json = JsonSerializer.Serialize(requestModel); + var requestBody = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); + + var responseMessage = await client.PostAsync(endpoint, requestBody); + + if (!responseMessage.IsSuccessStatusCode) + { + return (null, responseMessage); + } + + var responseContent = await responseMessage.Content.ReadAsStringAsync(); + + var response = JsonSerializer.Deserialize(responseContent); + + return (response, null); + } +} diff --git a/src/Admin/Billing/Models/ProcessStripeEvents/EventsFormModel.cs b/src/Admin/Billing/Models/ProcessStripeEvents/EventsFormModel.cs new file mode 100644 index 000000000..5ead00e26 --- /dev/null +++ b/src/Admin/Billing/Models/ProcessStripeEvents/EventsFormModel.cs @@ -0,0 +1,29 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Bit.Admin.Billing.Models.ProcessStripeEvents; + +public class EventsFormModel : IValidatableObject +{ + [Required] + public string EventIds { get; set; } + + [Required] + [DisplayName("Inspect Only")] + public bool Inspect { get; set; } + + public List GetEventIds() => + EventIds?.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries) + .Select(eventId => eventId.Trim()) + .ToList() ?? []; + + public IEnumerable Validate(ValidationContext validationContext) + { + var eventIds = GetEventIds(); + + if (eventIds.Any(eventId => !eventId.StartsWith("evt_"))) + { + yield return new ValidationResult("Event Ids must start with 'evt_'."); + } + } +} diff --git a/src/Admin/Billing/Models/ProcessStripeEvents/EventsRequestBody.cs b/src/Admin/Billing/Models/ProcessStripeEvents/EventsRequestBody.cs new file mode 100644 index 000000000..05a244460 --- /dev/null +++ b/src/Admin/Billing/Models/ProcessStripeEvents/EventsRequestBody.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Bit.Admin.Billing.Models.ProcessStripeEvents; + +public class EventsRequestBody +{ + [JsonPropertyName("eventIds")] + public List EventIds { get; set; } +} diff --git a/src/Admin/Billing/Models/ProcessStripeEvents/EventsResponseBody.cs b/src/Admin/Billing/Models/ProcessStripeEvents/EventsResponseBody.cs new file mode 100644 index 000000000..84eeb35d2 --- /dev/null +++ b/src/Admin/Billing/Models/ProcessStripeEvents/EventsResponseBody.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace Bit.Admin.Billing.Models.ProcessStripeEvents; + +public class EventsResponseBody +{ + [JsonPropertyName("events")] + public List Events { get; set; } + + [JsonIgnore] + public EventActionType ActionType { get; set; } +} + +public class EventResponseBody +{ + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("url")] + public string URL { get; set; } + + [JsonPropertyName("apiVersion")] + public string APIVersion { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("createdUTC")] + public DateTime CreatedUTC { get; set; } + + [JsonPropertyName("processingError")] + public string ProcessingError { get; set; } +} + +public enum EventActionType +{ + Inspect, + Process +} diff --git a/src/Admin/Billing/Views/ProcessStripeEvents/Index.cshtml b/src/Admin/Billing/Views/ProcessStripeEvents/Index.cshtml new file mode 100644 index 000000000..a8f5454d8 --- /dev/null +++ b/src/Admin/Billing/Views/ProcessStripeEvents/Index.cshtml @@ -0,0 +1,25 @@ +@model Bit.Admin.Billing.Models.ProcessStripeEvents.EventsFormModel + +@{ + ViewData["Title"] = "Process Stripe Events"; +} + +

Process Stripe Events

+
+
+
+
+ +
+
+
+
+ + +
+
+
+
+ +
+
diff --git a/src/Admin/Billing/Views/ProcessStripeEvents/Results.cshtml b/src/Admin/Billing/Views/ProcessStripeEvents/Results.cshtml new file mode 100644 index 000000000..2293f4833 --- /dev/null +++ b/src/Admin/Billing/Views/ProcessStripeEvents/Results.cshtml @@ -0,0 +1,49 @@ +@using Bit.Admin.Billing.Models.ProcessStripeEvents +@model Bit.Admin.Billing.Models.ProcessStripeEvents.EventsResponseBody + +@{ + var title = Model.ActionType == EventActionType.Inspect ? "Inspect Stripe Events" : "Process Stripe Events"; + ViewData["Title"] = title; +} + +

@title

+

Results

+ +
+ @if (!Model.Events.Any()) + { +

No data found.

+ } + else + { + + + + + + + + @if (Model.ActionType == EventActionType.Process) + { + + } + + + + @foreach (var eventResponseBody in Model.Events) + { + + + + + + @if (Model.ActionType == EventActionType.Process) + { + + } + + } + +
IDTypeAPI VersionCreatedProcessing Error
@eventResponseBody.Id@eventResponseBody.Type@eventResponseBody.APIVersion@eventResponseBody.CreatedUTC@eventResponseBody.ProcessingError
+ } +
diff --git a/src/Admin/Billing/Views/_ViewImports.cshtml b/src/Admin/Billing/Views/_ViewImports.cshtml new file mode 100644 index 000000000..02423ba0e --- /dev/null +++ b/src/Admin/Billing/Views/_ViewImports.cshtml @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Identity +@using Bit.Admin.AdminConsole +@using Bit.Admin.AdminConsole.Models +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper "*, Admin" diff --git a/src/Admin/Billing/Views/_ViewStart.cshtml b/src/Admin/Billing/Views/_ViewStart.cshtml new file mode 100644 index 000000000..820a2f6e0 --- /dev/null +++ b/src/Admin/Billing/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/src/Admin/Enums/Permissions.cs b/src/Admin/Enums/Permissions.cs index 6b73ba420..a8168b9e1 100644 --- a/src/Admin/Enums/Permissions.cs +++ b/src/Admin/Enums/Permissions.cs @@ -47,5 +47,6 @@ public enum Permission Tools_GenerateLicenseFile, Tools_ManageTaxRates, Tools_ManageStripeSubscriptions, - Tools_CreateEditTransaction + Tools_CreateEditTransaction, + Tools_ProcessStripeEvents } diff --git a/src/Admin/Startup.cs b/src/Admin/Startup.cs index 788908d42..f25e5072d 100644 --- a/src/Admin/Startup.cs +++ b/src/Admin/Startup.cs @@ -89,6 +89,7 @@ public class Startup services.AddDefaultServices(globalSettings); services.AddScoped(); services.AddBillingOperations(); + services.AddHttpClient(); #if OSS services.AddOosServices(); @@ -108,6 +109,7 @@ public class Startup { o.ViewLocationFormats.Add("/Auth/Views/{1}/{0}.cshtml"); o.ViewLocationFormats.Add("/AdminConsole/Views/{1}/{0}.cshtml"); + o.ViewLocationFormats.Add("/Billing/Views/{1}/{0}.cshtml"); }); // Jobs service diff --git a/src/Admin/Utilities/RolePermissionMapping.cs b/src/Admin/Utilities/RolePermissionMapping.cs index 0ca37f713..cb4a0fe47 100644 --- a/src/Admin/Utilities/RolePermissionMapping.cs +++ b/src/Admin/Utilities/RolePermissionMapping.cs @@ -161,7 +161,8 @@ public static class RolePermissionMapping Permission.Tools_GenerateLicenseFile, Permission.Tools_ManageTaxRates, Permission.Tools_ManageStripeSubscriptions, - Permission.Tools_CreateEditTransaction + Permission.Tools_CreateEditTransaction, + Permission.Tools_ProcessStripeEvents, } }, { "sales", new List diff --git a/src/Admin/Views/Shared/_Layout.cshtml b/src/Admin/Views/Shared/_Layout.cshtml index 7b204f48f..d3bfc6313 100644 --- a/src/Admin/Views/Shared/_Layout.cshtml +++ b/src/Admin/Views/Shared/_Layout.cshtml @@ -14,6 +14,7 @@ var canGenerateLicense = AccessControlService.UserHasPermission(Permission.Tools_GenerateLicenseFile); var canManageTaxRates = AccessControlService.UserHasPermission(Permission.Tools_ManageTaxRates); var canManageStripeSubscriptions = AccessControlService.UserHasPermission(Permission.Tools_ManageStripeSubscriptions); + var canProcessStripeEvents = AccessControlService.UserHasPermission(Permission.Tools_ProcessStripeEvents); var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin || canGenerateLicense || canManageTaxRates || canManageStripeSubscriptions; @@ -107,6 +108,12 @@ Manage Stripe Subscriptions } + @if (canProcessStripeEvents) + { + + Process Stripe Events + + } } diff --git a/src/Admin/appsettings.Development.json b/src/Admin/appsettings.Development.json index 645b9020d..861f9be98 100644 --- a/src/Admin/appsettings.Development.json +++ b/src/Admin/appsettings.Development.json @@ -13,7 +13,8 @@ "internalApi": "http://localhost:4000", "internalVault": "https://localhost:8080", "internalSso": "http://localhost:51822", - "internalScim": "http://localhost:44559" + "internalScim": "http://localhost:44559", + "internalBilling": "http://localhost:44519" }, "mail": { "smtp": { diff --git a/src/Billing/Controllers/RecoveryController.cs b/src/Billing/Controllers/RecoveryController.cs new file mode 100644 index 000000000..bada1e826 --- /dev/null +++ b/src/Billing/Controllers/RecoveryController.cs @@ -0,0 +1,68 @@ +using Bit.Billing.Models.Recovery; +using Bit.Billing.Services; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using Stripe; + +namespace Bit.Billing.Controllers; + +[Route("stripe/recovery")] +[SelfHosted(NotSelfHostedOnly = true)] +public class RecoveryController( + IStripeEventProcessor stripeEventProcessor, + IStripeFacade stripeFacade, + IWebHostEnvironment webHostEnvironment) : Controller +{ + private readonly string _stripeURL = webHostEnvironment.IsDevelopment() || webHostEnvironment.IsEnvironment("QA") + ? "https://dashboard.stripe.com/test" + : "https://dashboard.stripe.com"; + + // ReSharper disable once RouteTemplates.ActionRoutePrefixCanBeExtractedToControllerRoute + [HttpPost("events/inspect")] + public async Task> InspectEventsAsync([FromBody] EventsRequestBody requestBody) + { + var inspected = await Task.WhenAll(requestBody.EventIds.Select(async eventId => + { + var @event = await stripeFacade.GetEvent(eventId); + return Map(@event); + })); + + var response = new EventsResponseBody { Events = inspected.ToList() }; + + return TypedResults.Ok(response); + } + + // ReSharper disable once RouteTemplates.ActionRoutePrefixCanBeExtractedToControllerRoute + [HttpPost("events/process")] + public async Task> ProcessEventsAsync([FromBody] EventsRequestBody requestBody) + { + var processed = await Task.WhenAll(requestBody.EventIds.Select(async eventId => + { + var @event = await stripeFacade.GetEvent(eventId); + try + { + await stripeEventProcessor.ProcessEventAsync(@event); + return Map(@event); + } + catch (Exception exception) + { + return Map(@event, exception.Message); + } + })); + + var response = new EventsResponseBody { Events = processed.ToList() }; + + return TypedResults.Ok(response); + } + + private EventResponseBody Map(Event @event, string processingError = null) => new() + { + Id = @event.Id, + URL = $"{_stripeURL}/workbench/events/{@event.Id}", + APIVersion = @event.ApiVersion, + Type = @event.Type, + CreatedUTC = @event.Created, + ProcessingError = processingError + }; +} diff --git a/src/Billing/Models/Recovery/EventsRequestBody.cs b/src/Billing/Models/Recovery/EventsRequestBody.cs new file mode 100644 index 000000000..a40f8c965 --- /dev/null +++ b/src/Billing/Models/Recovery/EventsRequestBody.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Bit.Billing.Models.Recovery; + +public class EventsRequestBody +{ + [JsonPropertyName("eventIds")] + public List EventIds { get; set; } +} diff --git a/src/Billing/Models/Recovery/EventsResponseBody.cs b/src/Billing/Models/Recovery/EventsResponseBody.cs new file mode 100644 index 000000000..a0c7f087b --- /dev/null +++ b/src/Billing/Models/Recovery/EventsResponseBody.cs @@ -0,0 +1,31 @@ +using System.Text.Json.Serialization; + +namespace Bit.Billing.Models.Recovery; + +public class EventsResponseBody +{ + [JsonPropertyName("events")] + public List Events { get; set; } +} + +public class EventResponseBody +{ + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("url")] + public string URL { get; set; } + + [JsonPropertyName("apiVersion")] + public string APIVersion { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("createdUTC")] + public DateTime CreatedUTC { get; set; } + + [JsonPropertyName("processingError")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string ProcessingError { get; set; } +} diff --git a/src/Billing/Services/IStripeFacade.cs b/src/Billing/Services/IStripeFacade.cs index 0791e507f..f793846a5 100644 --- a/src/Billing/Services/IStripeFacade.cs +++ b/src/Billing/Services/IStripeFacade.cs @@ -16,6 +16,12 @@ public interface IStripeFacade RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + Task GetEvent( + string eventId, + EventGetOptions eventGetOptions = null, + RequestOptions requestOptions = null, + CancellationToken cancellationToken = default); + Task GetInvoice( string invoiceId, InvoiceGetOptions invoiceGetOptions = null, diff --git a/src/Billing/Services/Implementations/StripeFacade.cs b/src/Billing/Services/Implementations/StripeFacade.cs index 05ad9e0f4..420494678 100644 --- a/src/Billing/Services/Implementations/StripeFacade.cs +++ b/src/Billing/Services/Implementations/StripeFacade.cs @@ -6,6 +6,7 @@ public class StripeFacade : IStripeFacade { private readonly ChargeService _chargeService = new(); private readonly CustomerService _customerService = new(); + private readonly EventService _eventService = new(); private readonly InvoiceService _invoiceService = new(); private readonly PaymentMethodService _paymentMethodService = new(); private readonly SubscriptionService _subscriptionService = new(); @@ -19,6 +20,13 @@ public class StripeFacade : IStripeFacade CancellationToken cancellationToken = default) => await _chargeService.GetAsync(chargeId, chargeGetOptions, requestOptions, cancellationToken); + public async Task GetEvent( + string eventId, + EventGetOptions eventGetOptions = null, + RequestOptions requestOptions = null, + CancellationToken cancellationToken = default) => + await _eventService.GetAsync(eventId, eventGetOptions, requestOptions, cancellationToken); + public async Task GetCustomer( string customerId, CustomerGetOptions customerGetOptions = null, diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 42e3f2bdc..f99fb3b57 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -140,6 +140,7 @@ public class GlobalSettings : IGlobalSettings private string _internalSso; private string _internalVault; private string _internalScim; + private string _internalBilling; public BaseServiceUriSettings(GlobalSettings globalSettings) { @@ -218,6 +219,12 @@ public class GlobalSettings : IGlobalSettings get => _globalSettings.BuildInternalUri(_scim, "scim"); set => _internalScim = value; } + + public string InternalBilling + { + get => _globalSettings.BuildInternalUri(_internalBilling, "billing"); + set => _internalBilling = value; + } } public class SqlSettings diff --git a/src/Core/Settings/IBaseServiceUriSettings.cs b/src/Core/Settings/IBaseServiceUriSettings.cs index 0c2ed15f6..2a1d165ac 100644 --- a/src/Core/Settings/IBaseServiceUriSettings.cs +++ b/src/Core/Settings/IBaseServiceUriSettings.cs @@ -20,4 +20,5 @@ public interface IBaseServiceUriSettings public string InternalVault { get; set; } public string InternalSso { get; set; } public string InternalScim { get; set; } + public string InternalBilling { get; set; } } From 1199d72bfd9e043042d4c8e025fa9957dba4520b Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:51:49 -0400 Subject: [PATCH 03/29] Handle us_bank_account in charge.succeeded (#4807) --- .../Services/Implementations/StripeEventUtilityService.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Billing/Services/Implementations/StripeEventUtilityService.cs b/src/Billing/Services/Implementations/StripeEventUtilityService.cs index f656dbcc1..520205e74 100644 --- a/src/Billing/Services/Implementations/StripeEventUtilityService.cs +++ b/src/Billing/Services/Implementations/StripeEventUtilityService.cs @@ -206,6 +206,12 @@ public class StripeEventUtilityService : IStripeEventUtilityService transaction.PaymentMethodType = PaymentMethodType.Card; transaction.Details = $"{card.Brand?.ToUpperInvariant()}, *{card.Last4}"; } + else if (charge.PaymentMethodDetails.UsBankAccount != null) + { + var usBankAccount = charge.PaymentMethodDetails.UsBankAccount; + transaction.PaymentMethodType = PaymentMethodType.BankAccount; + transaction.Details = $"{usBankAccount.BankName}, *{usBankAccount.Last4}"; + } else if (charge.PaymentMethodDetails.AchDebit != null) { var achDebit = charge.PaymentMethodDetails.AchDebit; From 226f26a71500e235c560114fdddbc5fce060dcd2 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:52:21 -0400 Subject: [PATCH 04/29] Remove FF: AC-2828_provider-portal-members-page (#4805) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 3f4a16cb8..50e66386d 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -124,7 +124,6 @@ public static class FeatureFlagKeys public const string InlineMenuFieldQualification = "inline-menu-field-qualification"; public const string TwoFactorComponentRefactor = "two-factor-component-refactor"; public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements"; - public const string AC2828_ProviderPortalMembersPage = "AC-2828_provider-portal-members-page"; public const string ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner"; public const string DeviceTrustLogging = "pm-8285-device-trust-logging"; public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token"; From 8c8956da371688c75e4e4158e69251cdd7ff2b98 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 26 Sep 2024 16:04:27 -0400 Subject: [PATCH 05/29] [PM-12630] support for ping identity SCIM provisioning (#4804) * support for ping identity SCIM provisioning * mark ping ip list static --- .../src/Scim/Context/ScimContext.cs | 35 +++++++++++++++++++ .../src/Scim/Groups/PutGroupCommand.cs | 3 +- .../src/Scim/Users/GetUsersListQuery.cs | 7 ++-- .../AdminConsole/Enums/ScimProviderType.cs | 1 + 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/bitwarden_license/src/Scim/Context/ScimContext.cs b/bitwarden_license/src/Scim/Context/ScimContext.cs index 71ea27df4..efcc8dbde 100644 --- a/bitwarden_license/src/Scim/Context/ScimContext.cs +++ b/bitwarden_license/src/Scim/Context/ScimContext.cs @@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Settings; +using Bit.Core.Utilities; namespace Bit.Scim.Context; @@ -11,6 +12,32 @@ public class ScimContext : IScimContext { private bool _builtHttpContext; + // See IP list from Ping in docs: https://support.pingidentity.com/s/article/PingOne-IP-Addresses + private static readonly HashSet _pingIpAddresses = + [ + "18.217.152.87", + "52.14.10.143", + "13.58.49.148", + "34.211.92.81", + "54.214.158.219", + "34.218.98.164", + "15.223.133.47", + "3.97.84.38", + "15.223.19.71", + "3.97.98.120", + "52.60.115.173", + "3.97.202.223", + "18.184.65.93", + "52.57.244.92", + "18.195.7.252", + "108.128.67.71", + "34.246.158.102", + "108.128.250.27", + "52.63.103.92", + "13.54.131.18", + "52.62.204.36" + ]; + public ScimProviderType RequestScimProvider { get; set; } = ScimProviderType.Default; public ScimConfig ScimConfiguration { get; set; } public Guid? OrganizationId { get; set; } @@ -55,10 +82,18 @@ public class ScimContext : IScimContext RequestScimProvider = ScimProviderType.Okta; } } + if (RequestScimProvider == ScimProviderType.Default && httpContext.Request.Headers.ContainsKey("Adscimversion")) { RequestScimProvider = ScimProviderType.AzureAd; } + + var ipAddress = CoreHelpers.GetIpAddress(httpContext, globalSettings); + if (RequestScimProvider == ScimProviderType.Default && + _pingIpAddresses.Contains(ipAddress)) + { + RequestScimProvider = ScimProviderType.Ping; + } } } diff --git a/bitwarden_license/src/Scim/Groups/PutGroupCommand.cs b/bitwarden_license/src/Scim/Groups/PutGroupCommand.cs index d9cfc0d86..2503380a0 100644 --- a/bitwarden_license/src/Scim/Groups/PutGroupCommand.cs +++ b/bitwarden_license/src/Scim/Groups/PutGroupCommand.cs @@ -43,7 +43,8 @@ public class PutGroupCommand : IPutGroupCommand private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel model) { - if (_scimContext.RequestScimProvider != ScimProviderType.Okta) + if (_scimContext.RequestScimProvider != ScimProviderType.Okta && + _scimContext.RequestScimProvider != ScimProviderType.Ping) { return; } diff --git a/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs b/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs index 51250250f..1bea930f1 100644 --- a/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs +++ b/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs @@ -20,15 +20,16 @@ public class GetUsersListQuery : IGetUsersListQuery string externalIdFilter = null; if (!string.IsNullOrWhiteSpace(filter)) { - if (filter.StartsWith("userName eq ")) + var filterLower = filter.ToLowerInvariant(); + if (filterLower.StartsWith("username eq ")) { - usernameFilter = filter.Substring(12).Trim('"').ToLowerInvariant(); + usernameFilter = filterLower.Substring(12).Trim('"'); if (usernameFilter.Contains("@")) { emailFilter = usernameFilter; } } - else if (filter.StartsWith("externalId eq ")) + else if (filterLower.StartsWith("externalid eq ")) { externalIdFilter = filter.Substring(14).Trim('"'); } diff --git a/src/Core/AdminConsole/Enums/ScimProviderType.cs b/src/Core/AdminConsole/Enums/ScimProviderType.cs index 3f3fa7e6a..0ad11f54d 100644 --- a/src/Core/AdminConsole/Enums/ScimProviderType.cs +++ b/src/Core/AdminConsole/Enums/ScimProviderType.cs @@ -9,4 +9,5 @@ public enum ScimProviderType : byte JumpCloud = 4, GoogleWorkspace = 5, Rippling = 6, + Ping = 7, } From c66879eb8947a89ae72acc2f6862c8885aaf7c69 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Fri, 27 Sep 2024 19:39:44 +0100 Subject: [PATCH 06/29] [PM-8445] Update trial initiation UI (#4712) * Add the feature flag Signed-off-by: Cy Okeke * Initial comment Signed-off-by: Cy Okeke * changes to subscribe with payment method Signed-off-by: Cy Okeke * Add new objects * Implementation for subscription without payment method Signed-off-by: Cy Okeke * Remove unused codes and classes Signed-off-by: Cy Okeke * Rename the flag properly Signed-off-by: Cy Okeke * remove implementation that is no longer needed Signed-off-by: Cy Okeke * revert the changes on some code removal Signed-off-by: Cy Okeke * Resolve the pr comment Signed-off-by: Cy Okeke * format the data annotations line breaks Signed-off-by: Cy Okeke --------- Signed-off-by: Cy Okeke --- .../Controllers/OrganizationsController.cs | 15 +++ .../OrganizationCreateRequestModel.cs | 25 ++++ .../OrganizationNoPaymentCreateRequest.cs | 116 ++++++++++++++++++ .../Implementations/OrganizationService.cs | 18 ++- src/Core/Constants.cs | 1 + src/Core/Services/IPaymentService.cs | 3 + .../Implementations/StripePaymentService.cs | 71 +++++++++++ 7 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index 0715a3652..e5dbcd10b 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -171,6 +171,21 @@ public class OrganizationsController : Controller return new OrganizationResponseModel(result.Item1); } + [HttpPost("create-without-payment")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task CreateWithoutPaymentAsync([FromBody] OrganizationNoPaymentCreateRequest model) + { + var user = await _userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + var organizationSignup = model.ToOrganizationSignup(user); + var result = await _organizationService.SignUpAsync(organizationSignup); + return new OrganizationResponseModel(result.Item1); + } + [HttpPut("{id}")] [HttpPost("{id}")] public async Task Put(string id, [FromBody] OrganizationUpdateRequestModel model) diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs index 6f5e39b7d..539260a31 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs @@ -14,42 +14,63 @@ public class OrganizationCreateRequestModel : IValidatableObject [StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")] [JsonConverter(typeof(HtmlEncodingStringConverter))] public string Name { get; set; } + [StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")] [JsonConverter(typeof(HtmlEncodingStringConverter))] public string BusinessName { get; set; } + [Required] [StringLength(256)] [EmailAddress] public string BillingEmail { get; set; } + public PlanType PlanType { get; set; } + [Required] public string Key { get; set; } + public OrganizationKeysRequestModel Keys { get; set; } public PaymentMethodType? PaymentMethodType { get; set; } public string PaymentToken { get; set; } + [Range(0, int.MaxValue)] public int AdditionalSeats { get; set; } + [Range(0, 99)] public short? AdditionalStorageGb { get; set; } + public bool PremiumAccessAddon { get; set; } + [EncryptedString] [EncryptedStringLength(1000)] public string CollectionName { get; set; } + public string TaxIdNumber { get; set; } + public string BillingAddressLine1 { get; set; } + public string BillingAddressLine2 { get; set; } + public string BillingAddressCity { get; set; } + public string BillingAddressState { get; set; } + public string BillingAddressPostalCode { get; set; } + [StringLength(2)] public string BillingAddressCountry { get; set; } + public int? MaxAutoscaleSeats { get; set; } + [Range(0, int.MaxValue)] public int? AdditionalSmSeats { get; set; } + [Range(0, int.MaxValue)] public int? AdditionalServiceAccounts { get; set; } + [Required] public bool UseSecretsManager { get; set; } + public bool IsFromSecretsManagerTrial { get; set; } public string InitiationPath { get; set; } @@ -99,16 +120,19 @@ public class OrganizationCreateRequestModel : IValidatableObject { yield return new ValidationResult("Payment required.", new string[] { nameof(PaymentToken) }); } + if (PlanType != PlanType.Free && !PaymentMethodType.HasValue) { yield return new ValidationResult("Payment method type required.", new string[] { nameof(PaymentMethodType) }); } + if (PlanType != PlanType.Free && string.IsNullOrWhiteSpace(BillingAddressCountry)) { yield return new ValidationResult("Country required.", new string[] { nameof(BillingAddressCountry) }); } + if (PlanType != PlanType.Free && BillingAddressCountry == "US" && string.IsNullOrWhiteSpace(BillingAddressPostalCode)) { @@ -117,3 +141,4 @@ public class OrganizationCreateRequestModel : IValidatableObject } } } + diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs new file mode 100644 index 000000000..3255c8b41 --- /dev/null +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs @@ -0,0 +1,116 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using Bit.Core.Billing.Enums; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Business; +using Bit.Core.Utilities; + +namespace Bit.Api.AdminConsole.Models.Request.Organizations; + +public class OrganizationNoPaymentCreateRequest +{ + [Required] + [StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")] + [JsonConverter(typeof(HtmlEncodingStringConverter))] + public string Name { get; set; } + + [StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")] + [JsonConverter(typeof(HtmlEncodingStringConverter))] + public string BusinessName { get; set; } + + [Required] + [StringLength(256)] + [EmailAddress] + public string BillingEmail { get; set; } + + public PlanType PlanType { get; set; } + + [Required] + public string Key { get; set; } + + public OrganizationKeysRequestModel Keys { get; set; } + public PaymentMethodType? PaymentMethodType { get; set; } + public string PaymentToken { get; set; } + + [Range(0, int.MaxValue)] + public int AdditionalSeats { get; set; } + + [Range(0, 99)] + public short? AdditionalStorageGb { get; set; } + + public bool PremiumAccessAddon { get; set; } + + [EncryptedString] + [EncryptedStringLength(1000)] + public string CollectionName { get; set; } + + public string TaxIdNumber { get; set; } + + public string BillingAddressLine1 { get; set; } + + public string BillingAddressLine2 { get; set; } + + public string BillingAddressCity { get; set; } + + public string BillingAddressState { get; set; } + + public string BillingAddressPostalCode { get; set; } + + [StringLength(2)] + public string BillingAddressCountry { get; set; } + + public int? MaxAutoscaleSeats { get; set; } + + [Range(0, int.MaxValue)] + public int? AdditionalSmSeats { get; set; } + + [Range(0, int.MaxValue)] + public int? AdditionalServiceAccounts { get; set; } + + [Required] + public bool UseSecretsManager { get; set; } + + public bool IsFromSecretsManagerTrial { get; set; } + + public string InitiationPath { get; set; } + + public virtual OrganizationSignup ToOrganizationSignup(User user) + { + var orgSignup = new OrganizationSignup + { + Owner = user, + OwnerKey = Key, + Name = Name, + Plan = PlanType, + PaymentMethodType = PaymentMethodType, + PaymentToken = PaymentToken, + AdditionalSeats = AdditionalSeats, + MaxAutoscaleSeats = MaxAutoscaleSeats, + AdditionalStorageGb = AdditionalStorageGb.GetValueOrDefault(0), + PremiumAccessAddon = PremiumAccessAddon, + BillingEmail = BillingEmail, + BusinessName = BusinessName, + CollectionName = CollectionName, + AdditionalSmSeats = AdditionalSmSeats.GetValueOrDefault(), + AdditionalServiceAccounts = AdditionalServiceAccounts.GetValueOrDefault(), + UseSecretsManager = UseSecretsManager, + IsFromSecretsManagerTrial = IsFromSecretsManagerTrial, + TaxInfo = new TaxInfo + { + TaxIdNumber = TaxIdNumber, + BillingAddressLine1 = BillingAddressLine1, + BillingAddressLine2 = BillingAddressLine2, + BillingAddressCity = BillingAddressCity, + BillingAddressState = BillingAddressState, + BillingAddressPostalCode = BillingAddressPostalCode, + BillingAddressCountry = BillingAddressCountry, + }, + InitiationPath = InitiationPath, + }; + + Keys?.ToOrganizationSignup(orgSignup); + + return orgSignup; + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 3bf69cc07..6a0855c4e 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -590,10 +590,20 @@ public class OrganizationService : IOrganizationService } else { - await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value, - signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats, - signup.PremiumAccessAddon, signup.TaxInfo, signup.IsFromProvider, signup.AdditionalSmSeats.GetValueOrDefault(), - signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial); + if (signup.PaymentMethodType != null) + { + await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value, + signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats, + signup.PremiumAccessAddon, signup.TaxInfo, signup.IsFromProvider, signup.AdditionalSmSeats.GetValueOrDefault(), + signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial); + } + else + { + await _paymentService.PurchaseOrganizationNoPaymentMethod(organization, plan, signup.AdditionalSeats, + signup.PremiumAccessAddon, signup.AdditionalSmSeats.GetValueOrDefault(), + signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial); + } + } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 50e66386d..65c83da50 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -142,6 +142,7 @@ public static class FeatureFlagKeys public const string CipherKeyEncryption = "cipher-key-encryption"; public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill"; public const string StorageReseedRefactor = "storage-reseed-refactor"; + public const string TrialPayment = "PM-8163-trial-payment"; public static List GetAllKeys() { diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index bee69f9c6..bf9d04702 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -15,6 +15,9 @@ public interface IPaymentService string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false, int additionalSmSeats = 0, int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false); + Task PurchaseOrganizationNoPaymentMethod(Organization org, Plan plan, int additionalSeats, + bool premiumAccessAddon, int additionalSmSeats = 0, int additionalServiceAccount = 0, + bool signupIsFromSecretsManagerTrial = false); Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship); Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship); Task UpgradeFreeOrganizationAsync(Organization org, Plan plan, OrganizationUpgrade upgrade); diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index dd5adbda2..b31719a96 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -207,6 +207,77 @@ public class StripePaymentService : IPaymentService } } + public async Task PurchaseOrganizationNoPaymentMethod(Organization org, StaticStore.Plan plan, int additionalSeats, bool premiumAccessAddon, + int additionalSmSeats = 0, int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false) + { + + var stripeCustomerMetadata = new Dictionary + { + { "region", _globalSettings.BaseServiceUri.CloudRegion } + }; + var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, new TaxInfo(), additionalSeats, 0, premiumAccessAddon + , additionalSmSeats, additionalServiceAccount); + + Customer customer = null; + Subscription subscription; + try + { + var customerCreateOptions = new CustomerCreateOptions + { + Description = org.DisplayBusinessName(), + Email = org.BillingEmail, + Metadata = stripeCustomerMetadata, + InvoiceSettings = new CustomerInvoiceSettingsOptions + { + CustomFields = + [ + new CustomerInvoiceSettingsCustomFieldOptions + { + Name = org.SubscriberType(), + Value = GetFirstThirtyCharacters(org.SubscriberName()), + } + ], + }, + Coupon = signupIsFromSecretsManagerTrial + ? SecretsManagerStandaloneDiscountId + : null, + TaxIdData = null, + }; + + customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions); + subCreateOptions.AddExpand("latest_invoice.payment_intent"); + subCreateOptions.Customer = customer.Id; + + subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating customer, walking back operation."); + if (customer != null) + { + await _stripeAdapter.CustomerDeleteAsync(customer.Id); + } + + throw; + } + + org.Gateway = GatewayType.Stripe; + org.GatewayCustomerId = customer.Id; + org.GatewaySubscriptionId = subscription.Id; + + if (subscription.Status == "incomplete" && + subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action") + { + org.Enabled = false; + return subscription.LatestInvoice.PaymentIntent.ClientSecret; + } + + org.Enabled = true; + org.ExpirationDate = subscription.CurrentPeriodEnd; + return null; + + } + private async Task ChangeOrganizationSponsorship( Organization org, OrganizationSponsorship sponsorship, From bee83724a3a203b269c9bc0d285fcffa29bd1528 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 09:41:33 +0200 Subject: [PATCH 07/29] [deps] Billing: Update FluentAssertions to 6.12.1 (#4817) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- test/Billing.Test/Billing.Test.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Billing.Test/Billing.Test.csproj b/test/Billing.Test/Billing.Test.csproj index a30425e8f..3bbda52de 100644 --- a/test/Billing.Test/Billing.Test.csproj +++ b/test/Billing.Test/Billing.Test.csproj @@ -6,7 +6,7 @@ - + From 9eacf16ff6d50b6b82b1ccc892f1e10b943d7250 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 09:41:57 +0200 Subject: [PATCH 08/29] [deps] Billing: Update coverlet.collector to 6.0.2 (#4818) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../Infrastructure.Dapper.Test.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj b/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj index fba0791a3..30d2e1c6d 100644 --- a/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj +++ b/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj @@ -16,7 +16,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all From bbcdbd7498ebdb0a3bbe50f109ed212dd8a8294e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 09:42:20 +0200 Subject: [PATCH 09/29] [deps] Billing: Update Braintree to 5.27.0 (#4823) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index b87afb8fb..4d5c70fd2 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -54,7 +54,7 @@ - + From 222f8dd94951992846cfb215e90d17f6ba3b4a65 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 09:44:13 +0200 Subject: [PATCH 10/29] [deps] Billing: Update xunit-dotnet monorepo (#4827) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../Infrastructure.Dapper.Test.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj b/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj index 30d2e1c6d..db5913d72 100644 --- a/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj +++ b/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj @@ -11,8 +11,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all From f20f646a7eed473c50ce18dbc7febb700ea1753c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 09:45:06 +0200 Subject: [PATCH 11/29] [deps] Billing: Update swashbuckle-aspnetcore monorepo to 6.8.0 (#4826) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- src/Api/Api.csproj | 2 +- src/SharedWeb/SharedWeb.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 7945c3e7e..1b76bccf2 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "swashbuckle.aspnetcore.cli": { - "version": "6.7.3", + "version": "6.8.0", "commands": ["swagger"] }, "dotnet-ef": { diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index ce80c2eb1..4ca1c8344 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -35,7 +35,7 @@ - + diff --git a/src/SharedWeb/SharedWeb.csproj b/src/SharedWeb/SharedWeb.csproj index a17174dcc..a4c500d24 100644 --- a/src/SharedWeb/SharedWeb.csproj +++ b/src/SharedWeb/SharedWeb.csproj @@ -7,7 +7,7 @@ - + From 2e8a621293af2e92d74007b24480eb1fe7bac7e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 07:50:35 +0000 Subject: [PATCH 12/29] [deps] Billing: Update Stripe.net to 45.14.0 (#4825) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 4d5c70fd2..28f060433 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -55,7 +55,7 @@ - + From 793ef3aab8e9cef883d8c78caf62dc226e660579 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:36:51 +0200 Subject: [PATCH 13/29] [deps] DevOps: Update gh minor (#4828) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../workflows/_move_finalization_db_scripts.yml | 4 ++-- .github/workflows/build.yml | 14 +++++++------- .github/workflows/cleanup-rc-branch.yml | 2 +- .github/workflows/code-references.yml | 2 +- .github/workflows/protect-files.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/scan.yml | 6 +++--- .github/workflows/test-database.yml | 4 ++-- .github/workflows/test.yml | 2 +- .github/workflows/version-bump.yml | 4 ++-- 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/_move_finalization_db_scripts.yml b/.github/workflows/_move_finalization_db_scripts.yml index fc8a7b76e..c54e3abb2 100644 --- a/.github/workflows/_move_finalization_db_scripts.yml +++ b/.github/workflows/_move_finalization_db_scripts.yml @@ -30,7 +30,7 @@ jobs: secrets: "github-pat-bitwarden-devops-bot-repo-scope" - name: Check out branch - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} @@ -54,7 +54,7 @@ jobs: if: ${{ needs.setup.outputs.copy_finalization_scripts == 'true' }} steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: fetch-depth: 0 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a7bf370cc..fe4063f44 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 @@ -68,7 +68,7 @@ jobs: node: true steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 @@ -173,7 +173,7 @@ jobs: dotnet: true steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Check branch to publish env: @@ -263,7 +263,7 @@ jobs: -d ${{ matrix.base_path }}/${{ matrix.project_name }}/obj/build-output/publish - name: Build Docker image - uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0 + uses: docker/build-push-action@32945a339266b759abcbdc89316275140b0fc960 # v6.8.0 with: context: ${{ matrix.base_path }}/${{ matrix.project_name }} file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile @@ -282,7 +282,7 @@ jobs: output-format: sarif - name: Upload Grype results to GitHub - uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 + uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 with: sarif_file: ${{ steps.container-scan.outputs.sarif }} @@ -292,7 +292,7 @@ jobs: needs: build-docker steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 @@ -467,7 +467,7 @@ jobs: - win-x64 steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 diff --git a/.github/workflows/cleanup-rc-branch.yml b/.github/workflows/cleanup-rc-branch.yml index abd7c4bb4..3b3c2d55d 100644 --- a/.github/workflows/cleanup-rc-branch.yml +++ b/.github/workflows/cleanup-rc-branch.yml @@ -24,7 +24,7 @@ jobs: secrets: "github-pat-bitwarden-devops-bot-repo-scope" - name: Checkout main - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: ref: main token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} diff --git a/.github/workflows/code-references.yml b/.github/workflows/code-references.yml index 90523ba17..101e5730d 100644 --- a/.github/workflows/code-references.yml +++ b/.github/workflows/code-references.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Collect id: collect diff --git a/.github/workflows/protect-files.yml b/.github/workflows/protect-files.yml index 9e2e03d67..3bbc7e74f 100644 --- a/.github/workflows/protect-files.yml +++ b/.github/workflows/protect-files.yml @@ -29,7 +29,7 @@ jobs: label: "DB-migrations-changed" steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: fetch-depth: 2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 331f996c0..3c45f84b7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -99,7 +99,7 @@ jobs: echo "Github Release Option: $RELEASE_OPTION" - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up project name id: setup diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e0e8ae26..c63302cbc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,7 @@ jobs: fi - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Check release version id: version diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index b4335ee49..0f4d060ba 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: ref: ${{ github.event.pull_request.head.sha }} @@ -46,7 +46,7 @@ jobs: --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 + uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 with: sarif_file: cx_result.sarif @@ -66,7 +66,7 @@ jobs: distribution: "zulu" - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index ef02f8b70..325f10b94 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 @@ -147,7 +147,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1b4739df1..216130a21 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,7 +46,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 0fb8c4a22..e1d96ee4d 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -39,7 +39,7 @@ jobs: fi - name: Check out branch - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Check if RC branch exists if: ${{ inputs.cut_rc_branch == true }} @@ -230,7 +230,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out branch - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: ref: main From 81190c1bdf04072de4bc3dd02f20355c255dbd59 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Mon, 30 Sep 2024 12:35:14 +0200 Subject: [PATCH 14/29] PM-11602 | Error toast when expired org attempts to auto scale is unclear (#4746) --- src/Core/Services/Implementations/StripePaymentService.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index b31719a96..1720447b4 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -783,6 +783,11 @@ public class StripePaymentService : IPaymentService throw new GatewayException("Subscription not found."); } + if (sub.Status == SubscriptionStatuses.Canceled) + { + throw new BadRequestException("You do not have an active subscription. Reinstate your subscription to make changes."); + } + var collectionMethod = sub.CollectionMethod; var daysUntilDue = sub.DaysUntilDue; var chargeNow = collectionMethod == "charge_automatically"; From fa87c827fdec470bdbe18309efd6ef95f34b9915 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 13:52:32 +0200 Subject: [PATCH 15/29] [deps] Tools: Update aws-sdk-net monorepo (#4802) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 28f060433..0b09f696d 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -21,8 +21,8 @@ - - + + From 7b1edb3d3ff145494415e5ed14cc94f0327c59ff Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Mon, 30 Sep 2024 08:59:18 -0500 Subject: [PATCH 16/29] [PM-5450] Add check for admin/org access for events (#4705) * check to see if the org allows access to collections/ciphers to owners for events * linter * add check for organization value before attempting to use it * refactor logic to check for org abilities * remove checks for organization abilities - The previous logic would block events from being collected when a cipher was unassigned * check for organization when recording an event from owner/admin --- src/Events/Controllers/CollectController.cs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Events/Controllers/CollectController.cs b/src/Events/Controllers/CollectController.cs index 9e4ff531f..5e0417586 100644 --- a/src/Events/Controllers/CollectController.cs +++ b/src/Events/Controllers/CollectController.cs @@ -19,19 +19,22 @@ public class CollectController : Controller private readonly ICipherRepository _cipherRepository; private readonly IOrganizationRepository _organizationRepository; private readonly IFeatureService _featureService; + private readonly IApplicationCacheService _applicationCacheService; public CollectController( ICurrentContext currentContext, IEventService eventService, ICipherRepository cipherRepository, IOrganizationRepository organizationRepository, - IFeatureService featureService) + IFeatureService featureService, + IApplicationCacheService applicationCacheService) { _currentContext = currentContext; _eventService = eventService; _cipherRepository = cipherRepository; _organizationRepository = organizationRepository; _featureService = featureService; + _applicationCacheService = applicationCacheService; } [HttpPost] @@ -77,7 +80,21 @@ public class CollectController : Controller } if (cipher == null) { - continue; + // When the user cannot access the cipher directly, check if the organization allows for + // admin/owners access to all collections and the user can access the cipher from that perspective. + if (!eventModel.OrganizationId.HasValue) + { + continue; + } + + cipher = await _cipherRepository.GetByIdAsync(eventModel.CipherId.Value); + var cipherBelongsToOrg = cipher.OrganizationId == eventModel.OrganizationId; + var org = _currentContext.GetOrganization(eventModel.OrganizationId.Value); + + if (!cipherBelongsToOrg || org == null || cipher == null) + { + continue; + } } if (!ciphersCache.ContainsKey(eventModel.CipherId.Value)) { From 17099ddfc0a3d8b4cd49e24f3c84eae0cf1d623c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 16:34:57 +0200 Subject: [PATCH 17/29] [deps] Tools: Update MailKit to 4.8.0 (#4829) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 0b09f696d..4e3fc77a4 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -34,7 +34,7 @@ - + From 392ade534eddd7f6517e68f401b0e7b1ef929e15 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:08:49 -0400 Subject: [PATCH 18/29] [deps] Billing: Update Kralizek.AutoFixture.Extensions.MockHttp to v2 (#4831) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- test/Common/Common.csproj | 2 +- test/Core.Test/Core.Test.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Common/Common.csproj b/test/Common/Common.csproj index ef61e03ca..1893487d2 100644 --- a/test/Common/Common.csproj +++ b/test/Common/Common.csproj @@ -14,7 +14,7 @@ - + diff --git a/test/Core.Test/Core.Test.csproj b/test/Core.Test/Core.Test.csproj index d38040363..a7aaa2302 100644 --- a/test/Core.Test/Core.Test.csproj +++ b/test/Core.Test/Core.Test.csproj @@ -17,7 +17,7 @@ - + From 72b7f6c06588a49d8704f5e50e4b00b7ebbe3b6e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:14:17 -0400 Subject: [PATCH 19/29] [deps] Billing: Update dotnet monorepo (#4819) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../test/Scim.IntegrationTest/Scim.IntegrationTest.csproj | 2 +- test/Identity.IntegrationTest/Identity.IntegrationTest.csproj | 2 +- .../Infrastructure.IntegrationTest.csproj | 2 +- test/IntegrationTestCommon/IntegrationTestCommon.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj b/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj index 7ece41eca..a84813fd7 100644 --- a/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj +++ b/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj @@ -9,7 +9,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj b/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj index eb11e2d0a..10240727c 100644 --- a/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj +++ b/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj @@ -10,7 +10,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj b/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj index 88519403c..fd4c3be76 100644 --- a/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj +++ b/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj @@ -11,7 +11,7 @@ - + diff --git a/test/IntegrationTestCommon/IntegrationTestCommon.csproj b/test/IntegrationTestCommon/IntegrationTestCommon.csproj index cc42bb38a..664710560 100644 --- a/test/IntegrationTestCommon/IntegrationTestCommon.csproj +++ b/test/IntegrationTestCommon/IntegrationTestCommon.csproj @@ -5,7 +5,7 @@ - + From 81b151b1c0b178a0911054387a1b31574412bc7b Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Mon, 30 Sep 2024 13:21:30 -0500 Subject: [PATCH 20/29] [PM-12074] - Refactored `Index` to use `UserViewModel` (#4797) * Refactored View and Edit models to have all needed fields. --- src/Admin/Controllers/UsersController.cs | 44 +++-- src/Admin/Models/UserEditModel.cs | 39 +++-- src/Admin/Models/UserViewModel.cs | 123 +++++++++++++- src/Admin/Models/UsersModel.cs | 6 +- src/Admin/Views/Users/Edit.cshtml | 2 +- src/Admin/Views/Users/Index.cshtml | 154 ++++++++---------- src/Admin/Views/Users/View.cshtml | 6 +- src/Admin/Views/Users/_ViewInformation.cshtml | 25 ++- test/Admin.Test/Admin.Test.csproj | 1 + test/Admin.Test/Models/UserViewModelTests.cs | 108 ++++++++++++ test/Admin.Test/PlaceholderUnitTest.cs | 10 -- .../OrganizationUsersControllerTests.cs | 2 +- 12 files changed, 367 insertions(+), 153 deletions(-) create mode 100644 test/Admin.Test/Models/UserViewModelTests.cs delete mode 100644 test/Admin.Test/PlaceholderUnitTest.cs diff --git a/src/Admin/Controllers/UsersController.cs b/src/Admin/Controllers/UsersController.cs index e233b61e4..842abaea6 100644 --- a/src/Admin/Controllers/UsersController.cs +++ b/src/Admin/Controllers/UsersController.cs @@ -1,11 +1,11 @@ -using Bit.Admin.Enums; +#nullable enable + +using Bit.Admin.Enums; using Bit.Admin.Models; using Bit.Admin.Services; using Bit.Admin.Utilities; using Bit.Core; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; -using Bit.Core.Context; -using Bit.Core.Entities; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -24,9 +24,9 @@ public class UsersController : Controller private readonly IPaymentService _paymentService; private readonly GlobalSettings _globalSettings; private readonly IAccessControlService _accessControlService; - private readonly ICurrentContext _currentContext; - private readonly IFeatureService _featureService; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IFeatureService _featureService; + private readonly IUserService _userService; public UsersController( IUserRepository userRepository, @@ -34,18 +34,18 @@ public class UsersController : Controller IPaymentService paymentService, GlobalSettings globalSettings, IAccessControlService accessControlService, - ICurrentContext currentContext, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IFeatureService featureService, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) + IUserService userService) { _userRepository = userRepository; _cipherRepository = cipherRepository; _paymentService = paymentService; _globalSettings = globalSettings; _accessControlService = accessControlService; - _currentContext = currentContext; - _featureService = featureService; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + _featureService = featureService; + _userService = userService; } [RequirePermission(Permission.User_List_View)] @@ -64,19 +64,26 @@ public class UsersController : Controller var skip = (page - 1) * count; var users = await _userRepository.SearchAsync(email, skip, count); + var userModels = new List(); + if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)) { - var user2Fa = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(users.Select(u => u.Id))).ToList(); - // TempDataSerializer is having an issue serializing an empty IEnumerable>, do not set if empty. - if (user2Fa.Count != 0) + var twoFactorAuthLookup = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(users.Select(u => u.Id))).ToList(); + + userModels = UserViewModel.MapViewModels(users, twoFactorAuthLookup).ToList(); + } + else + { + foreach (var user in users) { - TempData["UsersTwoFactorIsEnabled"] = user2Fa; + var isTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + userModels.Add(UserViewModel.MapViewModel(user, isTwoFactorEnabled)); } } return View(new UsersModel { - Items = users as List, + Items = userModels, Email = string.IsNullOrWhiteSpace(email) ? null : email, Page = page, Count = count, @@ -87,13 +94,17 @@ public class UsersController : Controller public async Task View(Guid id) { var user = await _userRepository.GetByIdAsync(id); + if (user == null) { return RedirectToAction("Index"); } var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); - return View(new UserViewModel(user, ciphers)); + + var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); + + return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers)); } [SelfHosted(NotSelfHostedOnly = true)] @@ -108,7 +119,8 @@ public class UsersController : Controller var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); var billingInfo = await _paymentService.GetBillingAsync(user); var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user); - return View(new UserEditModel(user, ciphers, billingInfo, billingHistoryInfo, _globalSettings)); + var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); + return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings)); } [HttpPost] diff --git a/src/Admin/Models/UserEditModel.cs b/src/Admin/Models/UserEditModel.cs index f739af199..52cdb4c80 100644 --- a/src/Admin/Models/UserEditModel.cs +++ b/src/Admin/Models/UserEditModel.cs @@ -7,18 +7,23 @@ using Bit.Core.Vault.Entities; namespace Bit.Admin.Models; -public class UserEditModel : UserViewModel +public class UserEditModel { - public UserEditModel() { } + public UserEditModel() + { + + } public UserEditModel( User user, + bool isTwoFactorEnabled, IEnumerable ciphers, BillingInfo billingInfo, BillingHistoryInfo billingHistoryInfo, GlobalSettings globalSettings) - : base(user, ciphers) { + User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers); + BillingInfo = billingInfo; BillingHistoryInfo = billingHistoryInfo; BraintreeMerchantId = globalSettings.Braintree.MerchantId; @@ -35,32 +40,32 @@ public class UserEditModel : UserViewModel PremiumExpirationDate = user.PremiumExpirationDate; } - public BillingInfo BillingInfo { get; set; } - public BillingHistoryInfo BillingHistoryInfo { get; set; } + public UserViewModel User { get; init; } + public BillingInfo BillingInfo { get; init; } + public BillingHistoryInfo BillingHistoryInfo { get; init; } public string RandomLicenseKey => CoreHelpers.SecureRandomString(20); public string OneYearExpirationDate => DateTime.Now.AddYears(1).ToString("yyyy-MM-ddTHH:mm"); - public string BraintreeMerchantId { get; set; } + public string BraintreeMerchantId { get; init; } [Display(Name = "Name")] - public string Name { get; set; } + public string Name { get; init; } [Required] [Display(Name = "Email")] - public string Email { get; set; } + public string Email { get; init; } [Display(Name = "Email Verified")] - public bool EmailVerified { get; set; } + public bool EmailVerified { get; init; } [Display(Name = "Premium")] - public bool Premium { get; set; } + public bool Premium { get; init; } [Display(Name = "Max. Storage GB")] - public short? MaxStorageGb { get; set; } + public short? MaxStorageGb { get; init; } [Display(Name = "Gateway")] - public Core.Enums.GatewayType? Gateway { get; set; } + public Core.Enums.GatewayType? Gateway { get; init; } [Display(Name = "Gateway Customer Id")] - public string GatewayCustomerId { get; set; } + public string GatewayCustomerId { get; init; } [Display(Name = "Gateway Subscription Id")] - public string GatewaySubscriptionId { get; set; } + public string GatewaySubscriptionId { get; init; } [Display(Name = "License Key")] - public string LicenseKey { get; set; } + public string LicenseKey { get; init; } [Display(Name = "Premium Expiration Date")] - public DateTime? PremiumExpirationDate { get; set; } - + public DateTime? PremiumExpirationDate { get; init; } } diff --git a/src/Admin/Models/UserViewModel.cs b/src/Admin/Models/UserViewModel.cs index 05160f2e0..09b3d5577 100644 --- a/src/Admin/Models/UserViewModel.cs +++ b/src/Admin/Models/UserViewModel.cs @@ -1,18 +1,131 @@ using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Vault.Entities; namespace Bit.Admin.Models; public class UserViewModel { - public UserViewModel() { } + public Guid Id { get; } + public string Name { get; } + public string Email { get; } + public DateTime CreationDate { get; } + public DateTime? PremiumExpirationDate { get; } + public bool Premium { get; } + public short? MaxStorageGb { get; } + public bool EmailVerified { get; } + public bool TwoFactorEnabled { get; } + public DateTime AccountRevisionDate { get; } + public DateTime RevisionDate { get; } + public DateTime? LastEmailChangeDate { get; } + public DateTime? LastKdfChangeDate { get; } + public DateTime? LastKeyRotationDate { get; } + public DateTime? LastPasswordChangeDate { get; } + public GatewayType? Gateway { get; } + public string GatewayCustomerId { get; } + public string GatewaySubscriptionId { get; } + public string LicenseKey { get; } + public int CipherCount { get; set; } - public UserViewModel(User user, IEnumerable ciphers) + public UserViewModel(Guid id, + string name, + string email, + DateTime creationDate, + DateTime? premiumExpirationDate, + bool premium, + short? maxStorageGb, + bool emailVerified, + bool twoFactorEnabled, + DateTime accountRevisionDate, + DateTime revisionDate, + DateTime? lastEmailChangeDate, + DateTime? lastKdfChangeDate, + DateTime? lastKeyRotationDate, + DateTime? lastPasswordChangeDate, + GatewayType? gateway, + string gatewayCustomerId, + string gatewaySubscriptionId, + string licenseKey, + IEnumerable ciphers) { - User = user; + Id = id; + Name = name; + Email = email; + CreationDate = creationDate; + PremiumExpirationDate = premiumExpirationDate; + Premium = premium; + MaxStorageGb = maxStorageGb; + EmailVerified = emailVerified; + TwoFactorEnabled = twoFactorEnabled; + AccountRevisionDate = accountRevisionDate; + RevisionDate = revisionDate; + LastEmailChangeDate = lastEmailChangeDate; + LastKdfChangeDate = lastKdfChangeDate; + LastKeyRotationDate = lastKeyRotationDate; + LastPasswordChangeDate = lastPasswordChangeDate; + Gateway = gateway; + GatewayCustomerId = gatewayCustomerId; + GatewaySubscriptionId = gatewaySubscriptionId; + LicenseKey = licenseKey; CipherCount = ciphers.Count(); } - public User User { get; set; } - public int CipherCount { get; set; } + public static IEnumerable MapViewModels( + IEnumerable users, + IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup) => + users.Select(user => MapViewModel(user, lookup)); + + public static UserViewModel MapViewModel(User user, + IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup) => + new( + user.Id, + user.Name, + user.Email, + user.CreationDate, + user.PremiumExpirationDate, + user.Premium, + user.MaxStorageGb, + user.EmailVerified, + IsTwoFactorEnabled(user, lookup), + user.AccountRevisionDate, + user.RevisionDate, + user.LastEmailChangeDate, + user.LastKdfChangeDate, + user.LastKeyRotationDate, + user.LastPasswordChangeDate, + user.Gateway, + user.GatewayCustomerId ?? string.Empty, + user.GatewaySubscriptionId ?? string.Empty, + user.LicenseKey ?? string.Empty, + Array.Empty()); + + public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled) => + MapViewModel(user, isTwoFactorEnabled, Array.Empty()); + + public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable ciphers) => + new( + user.Id, + user.Name, + user.Email, + user.CreationDate, + user.PremiumExpirationDate, + user.Premium, + user.MaxStorageGb, + user.EmailVerified, + isTwoFactorEnabled, + user.AccountRevisionDate, + user.RevisionDate, + user.LastEmailChangeDate, + user.LastKdfChangeDate, + user.LastKeyRotationDate, + user.LastPasswordChangeDate, + user.Gateway, + user.GatewayCustomerId ?? string.Empty, + user.GatewaySubscriptionId ?? string.Empty, + user.LicenseKey ?? string.Empty, + ciphers); + + public static bool IsTwoFactorEnabled(User user, + IEnumerable<(Guid userId, bool twoFactorIsEnabled)> twoFactorIsEnabledLookup) => + twoFactorIsEnabledLookup.FirstOrDefault(x => x.userId == user.Id).twoFactorIsEnabled; } diff --git a/src/Admin/Models/UsersModel.cs b/src/Admin/Models/UsersModel.cs index 0a54e318d..33148301b 100644 --- a/src/Admin/Models/UsersModel.cs +++ b/src/Admin/Models/UsersModel.cs @@ -1,8 +1,6 @@ -using Bit.Core.Entities; +namespace Bit.Admin.Models; -namespace Bit.Admin.Models; - -public class UsersModel : PagedModel +public class UsersModel : PagedModel { public string Email { get; set; } public string Action { get; set; } diff --git a/src/Admin/Views/Users/Edit.cshtml b/src/Admin/Views/Users/Edit.cshtml index 2bc326d22..8f07b12a7 100644 --- a/src/Admin/Views/Users/Edit.cshtml +++ b/src/Admin/Views/Users/Edit.cshtml @@ -86,7 +86,7 @@ @if (canViewUserInformation) {

User Information

- @await Html.PartialAsync("_ViewInformation", Model) + @await Html.PartialAsync("_ViewInformation", Model.User) } @if (canViewBillingInformation) { diff --git a/src/Admin/Views/Users/Index.cshtml b/src/Admin/Views/Users/Index.cshtml index 46419503f..a53580350 100644 --- a/src/Admin/Views/Users/Index.cshtml +++ b/src/Admin/Views/Users/Index.cshtml @@ -1,6 +1,4 @@ @model UsersModel -@inject Bit.Core.Services.IUserService userService -@inject Bit.Core.Services.IFeatureService featureService @{ ViewData["Title"] = "Users"; } @@ -16,100 +14,88 @@
- - - - - + + + + + - @if(!Model.Items.Any()) + @if (!Model.Items.Any()) + { + + + + } + else + { + @foreach (var user in Model.Items) { - + + + } - else - { - @foreach(var user in Model.Items) - { - - - - - - } - } + }
EmailCreatedDetails
EmailCreatedDetails
No results to list.
No results to list. + @user.Email + + + @user.CreationDate.ToShortDateString() + + + @if (user.Premium) + { + + + } + else + { + + } + @if (user.MaxStorageGb.HasValue && user.MaxStorageGb > 1) + { + + + } + else + { + + + } + @if (user.EmailVerified) + { + + } + else + { + + } + @if (user.TwoFactorEnabled) + { + + } + else + { + + } +
- @user.Email - - - @user.CreationDate.ToShortDateString() - - - @if(user.Premium) - { - - } - else - { - - } - @if(user.MaxStorageGb.HasValue && user.MaxStorageGb > 1) - { - - } - else - { - - } - @if(user.EmailVerified) - { - - } - else - { - - } - @if (featureService.IsEnabled(Bit.Core.FeatureFlagKeys.MembersTwoFAQueryOptimization)) - { - var usersTwoFactorIsEnabled = TempData["UsersTwoFactorIsEnabled"] as IEnumerable<(Guid userId, bool twoFactorIsEnabled)>; - var matchingUser2Fa = usersTwoFactorIsEnabled?.FirstOrDefault(tuple => tuple.userId == user.Id); - - @if(matchingUser2Fa is { twoFactorIsEnabled: true }) - { - - } - else - { - - } - } - else - { - @if(await userService.TwoFactorIsEnabledAsync(user)) - { - - } - else - { - - } - } -