Here's what that means:
+ - Your Bitwarden account is owned by {{OrganizationName}}
- Your administrators can delete your account at any time
- You cannot leave the organization
diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/NotifyAdminDeviceApprovalRequested.html.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/NotifyAdminDeviceApprovalRequested.html.hbs
new file mode 100644
index 0000000000..a54773a15e
--- /dev/null
+++ b/src/Core/MailTemplates/Handlebars/AdminConsole/NotifyAdminDeviceApprovalRequested.html.hbs
@@ -0,0 +1,23 @@
+{{#>FullHtmlLayout}}
+
+
+
+ {{UserNameRequestingAccess}} has sent a device approval request. Review login requests to allow the member to finish logging in.
+
+
+ |
+
+
+
+
+ Review request
+
+
+ |
+
+
+{{/FullHtmlLayout}}
diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/NotifyAdminDeviceApprovalRequested.text.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/NotifyAdminDeviceApprovalRequested.text.hbs
new file mode 100644
index 0000000000..e396546646
--- /dev/null
+++ b/src/Core/MailTemplates/Handlebars/AdminConsole/NotifyAdminDeviceApprovalRequested.text.hbs
@@ -0,0 +1,5 @@
+{{#>BasicTextLayout}}
+{{UserNameRequestingAccess}} has sent a device approval request. Review login requests to allow the member to finish logging in.
+
+{{Url}}
+{{/BasicTextLayout}}
diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/SelfHostNotifyAdminDeviceApprovalRequested.html.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/SelfHostNotifyAdminDeviceApprovalRequested.html.hbs
new file mode 100644
index 0000000000..ee7fcf8cad
--- /dev/null
+++ b/src/Core/MailTemplates/Handlebars/AdminConsole/SelfHostNotifyAdminDeviceApprovalRequested.html.hbs
@@ -0,0 +1,29 @@
+{{#>FullHtmlLayout}}
+
+
+
+ {{UserNameRequestingAccess}} has sent a device approval request. Review login requests to allow the member to finish logging in.
+ |
+
+
+
+ To review requests, log in to your self-hosted instance → navigate to the Admin Console → select Device Approvals
+
+
+ |
+
+
+
+
+ Review request
+
+
+ |
+
+
+{{/FullHtmlLayout}}
diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/SelfHostNotifyAdminDeviceApprovalRequested.text.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/SelfHostNotifyAdminDeviceApprovalRequested.text.hbs
new file mode 100644
index 0000000000..e5b412cc87
--- /dev/null
+++ b/src/Core/MailTemplates/Handlebars/AdminConsole/SelfHostNotifyAdminDeviceApprovalRequested.text.hbs
@@ -0,0 +1,7 @@
+{{#>BasicTextLayout}}
+{{UserNameRequestingAccess}} has sent a device approval request. Review login requests to allow the member to finish logging in.
+
+To review requests, log in to your self-hosted instance -> navigate to the Admin Console -> select Device Approvals.
+
+{{Url}}
+{{/BasicTextLayout}}
diff --git a/src/Core/MailTemplates/Handlebars/Layouts/Full.html.hbs b/src/Core/MailTemplates/Handlebars/Layouts/Full.html.hbs
index 5e4c0eb0ae..7ed9fb7d1a 100644
--- a/src/Core/MailTemplates/Handlebars/Layouts/Full.html.hbs
+++ b/src/Core/MailTemplates/Handlebars/Layouts/Full.html.hbs
@@ -130,7 +130,7 @@
@@ -148,13 +148,13 @@
|
-  |
-  |
-  |
-  |
-  |
-  |
-  |
+  |
+  |
+  |
+  |
+  |
+  |
+  |
|
diff --git a/src/Core/MailTemplates/Handlebars/Layouts/FullUpdated.html.hbs b/src/Core/MailTemplates/Handlebars/Layouts/FullUpdated.html.hbs
index f0a8688a41..f5772d61f6 100644
--- a/src/Core/MailTemplates/Handlebars/Layouts/FullUpdated.html.hbs
+++ b/src/Core/MailTemplates/Handlebars/Layouts/FullUpdated.html.hbs
@@ -107,10 +107,10 @@
.footer-text {
width: 100% !important;
- }
-
- .center {
- text-align: center !important;
+ }
+
+ .center {
+ text-align: center !important;
}
.templateColumnContainer{
@@ -159,7 +159,7 @@
@@ -177,13 +177,13 @@
-  |
-  |
-  |
-  |
-  |
-  |
-  |
+  |
+  |
+  |
+  |
+  |
+  |
+  |
|
diff --git a/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs b/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs
index 580c1c3b60..ee787dd083 100644
--- a/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs
+++ b/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs
@@ -15,4 +15,5 @@ public class PushRegistrationRequestModel
public DeviceType Type { get; set; }
[Required]
public string Identifier { get; set; }
+ public IEnumerable OrganizationIds { get; set; }
}
diff --git a/src/Core/Models/Api/Request/PushSendRequestModel.cs b/src/Core/Models/Api/Request/PushSendRequestModel.cs
index b85c8fb555..7247e6d25f 100644
--- a/src/Core/Models/Api/Request/PushSendRequestModel.cs
+++ b/src/Core/Models/Api/Request/PushSendRequestModel.cs
@@ -1,18 +1,18 @@
-using System.ComponentModel.DataAnnotations;
+#nullable enable
+using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
namespace Bit.Core.Models.Api;
public class PushSendRequestModel : IValidatableObject
{
- public string UserId { get; set; }
- public string OrganizationId { get; set; }
- public string DeviceId { get; set; }
- public string Identifier { get; set; }
- [Required]
- public PushType? Type { get; set; }
- [Required]
- public object Payload { get; set; }
+ public string? UserId { get; set; }
+ public string? OrganizationId { get; set; }
+ public string? DeviceId { get; set; }
+ public string? Identifier { get; set; }
+ public required PushType Type { get; set; }
+ public required object Payload { get; set; }
+ public ClientType? ClientType { get; set; }
public IEnumerable Validate(ValidationContext validationContext)
{
diff --git a/src/Core/Models/Business/SubscriptionCreateOptions.cs b/src/Core/Models/Business/SubscriptionCreateOptions.cs
index 64626780ef..2d42ee66f7 100644
--- a/src/Core/Models/Business/SubscriptionCreateOptions.cs
+++ b/src/Core/Models/Business/SubscriptionCreateOptions.cs
@@ -34,11 +34,6 @@ public class OrganizationSubscriptionOptionsBase : SubscriptionCreateOptions
AddPremiumAccessAddon(plan, premiumAccessAddon);
AddPasswordManagerSeat(plan, additionalSeats);
AddAdditionalStorage(plan, additionalStorageGb);
-
- if (!string.IsNullOrWhiteSpace(taxInfo?.StripeTaxRateId))
- {
- DefaultTaxRates = new List { taxInfo.StripeTaxRateId };
- }
}
private void AddSecretsManagerSeat(Plan plan, int additionalSmSeats)
diff --git a/src/Core/Models/Business/TaxInfo.cs b/src/Core/Models/Business/TaxInfo.cs
index b12c5229b3..80a63473a7 100644
--- a/src/Core/Models/Business/TaxInfo.cs
+++ b/src/Core/Models/Business/TaxInfo.cs
@@ -5,17 +5,10 @@ public class TaxInfo
public string TaxIdNumber { get; set; }
public string TaxIdType { get; set; }
- public string StripeTaxRateId { 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; }
public string BillingAddressCountry { get; set; } = "US";
-
- public bool HasTaxId
- {
- get => !string.IsNullOrWhiteSpace(TaxIdNumber) &&
- !string.IsNullOrWhiteSpace(TaxIdType);
- }
}
diff --git a/src/Core/Models/PushNotification.cs b/src/Core/Models/PushNotification.cs
index 9907abcb65..775c3443f2 100644
--- a/src/Core/Models/PushNotification.cs
+++ b/src/Core/Models/PushNotification.cs
@@ -1,10 +1,12 @@
-using Bit.Core.Enums;
+#nullable enable
+using Bit.Core.Enums;
+using Bit.Core.NotificationCenter.Enums;
namespace Bit.Core.Models;
public class PushNotificationData
{
- public PushNotificationData(PushType type, T payload, string contextId)
+ public PushNotificationData(PushType type, T payload, string? contextId)
{
Type = type;
Payload = payload;
@@ -13,7 +15,7 @@ public class PushNotificationData
public PushType Type { get; set; }
public T Payload { get; set; }
- public string ContextId { get; set; }
+ public string? ContextId { get; set; }
}
public class SyncCipherPushNotification
@@ -21,7 +23,7 @@ public class SyncCipherPushNotification
public Guid Id { get; set; }
public Guid? UserId { get; set; }
public Guid? OrganizationId { get; set; }
- public IEnumerable CollectionIds { get; set; }
+ public IEnumerable? CollectionIds { get; set; }
public DateTime RevisionDate { get; set; }
}
@@ -45,6 +47,22 @@ public class SyncSendPushNotification
public DateTime RevisionDate { get; set; }
}
+public class NotificationPushNotification
+{
+ public Guid Id { get; set; }
+ public Priority Priority { get; set; }
+ public bool Global { get; set; }
+ public ClientType ClientType { get; set; }
+ public Guid? UserId { get; set; }
+ public Guid? OrganizationId { get; set; }
+ public string? Title { get; set; }
+ public string? Body { get; set; }
+ public DateTime CreationDate { get; set; }
+ public DateTime RevisionDate { get; set; }
+ public DateTime? ReadDate { get; set; }
+ public DateTime? DeletedDate { get; set; }
+}
+
public class AuthRequestPushNotification
{
public Guid UserId { get; set; }
@@ -62,4 +80,5 @@ public class OrganizationCollectionManagementPushNotification
public Guid OrganizationId { get; init; }
public bool LimitCollectionCreation { get; init; }
public bool LimitCollectionDeletion { get; init; }
+ public bool LimitItemDeletion { get; init; }
}
diff --git a/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs b/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs
index 4f76950a34..3fddafcdc7 100644
--- a/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs
+++ b/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs
@@ -4,6 +4,7 @@ using Bit.Core.NotificationCenter.Authorization;
using Bit.Core.NotificationCenter.Commands.Interfaces;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Repositories;
+using Bit.Core.Platform.Push;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
@@ -14,14 +15,17 @@ public class CreateNotificationCommand : ICreateNotificationCommand
private readonly ICurrentContext _currentContext;
private readonly IAuthorizationService _authorizationService;
private readonly INotificationRepository _notificationRepository;
+ private readonly IPushNotificationService _pushNotificationService;
public CreateNotificationCommand(ICurrentContext currentContext,
IAuthorizationService authorizationService,
- INotificationRepository notificationRepository)
+ INotificationRepository notificationRepository,
+ IPushNotificationService pushNotificationService)
{
_currentContext = currentContext;
_authorizationService = authorizationService;
_notificationRepository = notificationRepository;
+ _pushNotificationService = pushNotificationService;
}
public async Task CreateAsync(Notification notification)
@@ -31,6 +35,10 @@ public class CreateNotificationCommand : ICreateNotificationCommand
await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notification,
NotificationOperations.Create);
- return await _notificationRepository.CreateAsync(notification);
+ var newNotification = await _notificationRepository.CreateAsync(notification);
+
+ await _pushNotificationService.PushNotificationAsync(newNotification);
+
+ return newNotification;
}
}
diff --git a/src/Core/NotificationCenter/Commands/CreateNotificationStatusCommand.cs b/src/Core/NotificationCenter/Commands/CreateNotificationStatusCommand.cs
index fcd61ceebc..793da22f81 100644
--- a/src/Core/NotificationCenter/Commands/CreateNotificationStatusCommand.cs
+++ b/src/Core/NotificationCenter/Commands/CreateNotificationStatusCommand.cs
@@ -5,6 +5,7 @@ using Bit.Core.NotificationCenter.Authorization;
using Bit.Core.NotificationCenter.Commands.Interfaces;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Repositories;
+using Bit.Core.Platform.Push;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
@@ -16,16 +17,19 @@ public class CreateNotificationStatusCommand : ICreateNotificationStatusCommand
private readonly IAuthorizationService _authorizationService;
private readonly INotificationRepository _notificationRepository;
private readonly INotificationStatusRepository _notificationStatusRepository;
+ private readonly IPushNotificationService _pushNotificationService;
public CreateNotificationStatusCommand(ICurrentContext currentContext,
IAuthorizationService authorizationService,
INotificationRepository notificationRepository,
- INotificationStatusRepository notificationStatusRepository)
+ INotificationStatusRepository notificationStatusRepository,
+ IPushNotificationService pushNotificationService)
{
_currentContext = currentContext;
_authorizationService = authorizationService;
_notificationRepository = notificationRepository;
_notificationStatusRepository = notificationStatusRepository;
+ _pushNotificationService = pushNotificationService;
}
public async Task CreateAsync(NotificationStatus notificationStatus)
@@ -42,6 +46,10 @@ public class CreateNotificationStatusCommand : ICreateNotificationStatusCommand
await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus,
NotificationStatusOperations.Create);
- return await _notificationStatusRepository.CreateAsync(notificationStatus);
+ var newNotificationStatus = await _notificationStatusRepository.CreateAsync(notificationStatus);
+
+ await _pushNotificationService.PushNotificationStatusAsync(notification, newNotificationStatus);
+
+ return newNotificationStatus;
}
}
diff --git a/src/Core/NotificationCenter/Commands/MarkNotificationDeletedCommand.cs b/src/Core/NotificationCenter/Commands/MarkNotificationDeletedCommand.cs
index 2ca7aa9051..256702c10c 100644
--- a/src/Core/NotificationCenter/Commands/MarkNotificationDeletedCommand.cs
+++ b/src/Core/NotificationCenter/Commands/MarkNotificationDeletedCommand.cs
@@ -5,6 +5,7 @@ using Bit.Core.NotificationCenter.Authorization;
using Bit.Core.NotificationCenter.Commands.Interfaces;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Repositories;
+using Bit.Core.Platform.Push;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
@@ -16,16 +17,19 @@ public class MarkNotificationDeletedCommand : IMarkNotificationDeletedCommand
private readonly IAuthorizationService _authorizationService;
private readonly INotificationRepository _notificationRepository;
private readonly INotificationStatusRepository _notificationStatusRepository;
+ private readonly IPushNotificationService _pushNotificationService;
public MarkNotificationDeletedCommand(ICurrentContext currentContext,
IAuthorizationService authorizationService,
INotificationRepository notificationRepository,
- INotificationStatusRepository notificationStatusRepository)
+ INotificationStatusRepository notificationStatusRepository,
+ IPushNotificationService pushNotificationService)
{
_currentContext = currentContext;
_authorizationService = authorizationService;
_notificationRepository = notificationRepository;
_notificationStatusRepository = notificationStatusRepository;
+ _pushNotificationService = pushNotificationService;
}
public async Task MarkDeletedAsync(Guid notificationId)
@@ -59,7 +63,9 @@ public class MarkNotificationDeletedCommand : IMarkNotificationDeletedCommand
await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus,
NotificationStatusOperations.Create);
- await _notificationStatusRepository.CreateAsync(notificationStatus);
+ var newNotificationStatus = await _notificationStatusRepository.CreateAsync(notificationStatus);
+
+ await _pushNotificationService.PushNotificationStatusAsync(notification, newNotificationStatus);
}
else
{
@@ -69,6 +75,8 @@ public class MarkNotificationDeletedCommand : IMarkNotificationDeletedCommand
notificationStatus.DeletedDate = DateTime.UtcNow;
await _notificationStatusRepository.UpdateAsync(notificationStatus);
+
+ await _pushNotificationService.PushNotificationStatusAsync(notification, notificationStatus);
}
}
}
diff --git a/src/Core/NotificationCenter/Commands/MarkNotificationReadCommand.cs b/src/Core/NotificationCenter/Commands/MarkNotificationReadCommand.cs
index 400e44463a..9c9d1d48a2 100644
--- a/src/Core/NotificationCenter/Commands/MarkNotificationReadCommand.cs
+++ b/src/Core/NotificationCenter/Commands/MarkNotificationReadCommand.cs
@@ -5,6 +5,7 @@ using Bit.Core.NotificationCenter.Authorization;
using Bit.Core.NotificationCenter.Commands.Interfaces;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Repositories;
+using Bit.Core.Platform.Push;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
@@ -16,16 +17,19 @@ public class MarkNotificationReadCommand : IMarkNotificationReadCommand
private readonly IAuthorizationService _authorizationService;
private readonly INotificationRepository _notificationRepository;
private readonly INotificationStatusRepository _notificationStatusRepository;
+ private readonly IPushNotificationService _pushNotificationService;
public MarkNotificationReadCommand(ICurrentContext currentContext,
IAuthorizationService authorizationService,
INotificationRepository notificationRepository,
- INotificationStatusRepository notificationStatusRepository)
+ INotificationStatusRepository notificationStatusRepository,
+ IPushNotificationService pushNotificationService)
{
_currentContext = currentContext;
_authorizationService = authorizationService;
_notificationRepository = notificationRepository;
_notificationStatusRepository = notificationStatusRepository;
+ _pushNotificationService = pushNotificationService;
}
public async Task MarkReadAsync(Guid notificationId)
@@ -59,7 +63,9 @@ public class MarkNotificationReadCommand : IMarkNotificationReadCommand
await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus,
NotificationStatusOperations.Create);
- await _notificationStatusRepository.CreateAsync(notificationStatus);
+ var newNotificationStatus = await _notificationStatusRepository.CreateAsync(notificationStatus);
+
+ await _pushNotificationService.PushNotificationStatusAsync(notification, newNotificationStatus);
}
else
{
@@ -69,6 +75,8 @@ public class MarkNotificationReadCommand : IMarkNotificationReadCommand
notificationStatus.ReadDate = DateTime.UtcNow;
await _notificationStatusRepository.UpdateAsync(notificationStatus);
+
+ await _pushNotificationService.PushNotificationStatusAsync(notification, notificationStatus);
}
}
}
diff --git a/src/Core/NotificationCenter/Commands/UpdateNotificationCommand.cs b/src/Core/NotificationCenter/Commands/UpdateNotificationCommand.cs
index f049478178..471786aac6 100644
--- a/src/Core/NotificationCenter/Commands/UpdateNotificationCommand.cs
+++ b/src/Core/NotificationCenter/Commands/UpdateNotificationCommand.cs
@@ -5,6 +5,7 @@ using Bit.Core.NotificationCenter.Authorization;
using Bit.Core.NotificationCenter.Commands.Interfaces;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Repositories;
+using Bit.Core.Platform.Push;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
@@ -15,14 +16,17 @@ public class UpdateNotificationCommand : IUpdateNotificationCommand
private readonly ICurrentContext _currentContext;
private readonly IAuthorizationService _authorizationService;
private readonly INotificationRepository _notificationRepository;
+ private readonly IPushNotificationService _pushNotificationService;
public UpdateNotificationCommand(ICurrentContext currentContext,
IAuthorizationService authorizationService,
- INotificationRepository notificationRepository)
+ INotificationRepository notificationRepository,
+ IPushNotificationService pushNotificationService)
{
_currentContext = currentContext;
_authorizationService = authorizationService;
_notificationRepository = notificationRepository;
+ _pushNotificationService = pushNotificationService;
}
public async Task UpdateAsync(Notification notificationToUpdate)
@@ -43,5 +47,7 @@ public class UpdateNotificationCommand : IUpdateNotificationCommand
notification.RevisionDate = DateTime.UtcNow;
await _notificationRepository.ReplaceAsync(notification);
+
+ await _pushNotificationService.PushNotificationAsync(notification);
}
}
diff --git a/src/Core/NotificationCenter/Entities/Notification.cs b/src/Core/NotificationCenter/Entities/Notification.cs
index 7ab3187524..ad43299f55 100644
--- a/src/Core/NotificationCenter/Entities/Notification.cs
+++ b/src/Core/NotificationCenter/Entities/Notification.cs
@@ -15,11 +15,11 @@ public class Notification : ITableObject
public ClientType ClientType { get; set; }
public Guid? UserId { get; set; }
public Guid? OrganizationId { get; set; }
- [MaxLength(256)]
- public string? Title { get; set; }
- public string? Body { get; set; }
+ [MaxLength(256)] public string? Title { get; set; }
+ [MaxLength(3000)] public string? Body { get; set; }
public DateTime CreationDate { get; set; }
public DateTime RevisionDate { get; set; }
+ public Guid? TaskId { get; set; }
public void SetNewId()
{
diff --git a/src/Core/NotificationHub/INotificationHubPool.cs b/src/Core/NotificationHub/INotificationHubPool.cs
index 7c383d7b96..18bae98bc6 100644
--- a/src/Core/NotificationHub/INotificationHubPool.cs
+++ b/src/Core/NotificationHub/INotificationHubPool.cs
@@ -4,6 +4,6 @@ namespace Bit.Core.NotificationHub;
public interface INotificationHubPool
{
- NotificationHubClient ClientFor(Guid comb);
+ INotificationHubClient ClientFor(Guid comb);
INotificationHubProxy AllClients { get; }
}
diff --git a/src/Core/NotificationHub/NotificationHubPool.cs b/src/Core/NotificationHub/NotificationHubPool.cs
index 7448aad5bd..8993ee2b8e 100644
--- a/src/Core/NotificationHub/NotificationHubPool.cs
+++ b/src/Core/NotificationHub/NotificationHubPool.cs
@@ -43,7 +43,7 @@ public class NotificationHubPool : INotificationHubPool
///
///
/// Thrown when no notification hub is found for a given comb.
- public NotificationHubClient ClientFor(Guid comb)
+ public INotificationHubClient ClientFor(Guid comb)
{
var possibleConnections = _connections.Where(c => c.RegistrationEnabled(comb)).ToArray();
if (possibleConnections.Length == 0)
diff --git a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs
index d90ebdd744..7baf0352ee 100644
--- a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs
+++ b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs
@@ -1,4 +1,5 @@
-using System.Text.Json;
+#nullable enable
+using System.Text.Json;
using System.Text.RegularExpressions;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
@@ -6,12 +7,14 @@ using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.Models.Data;
+using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
+using Notification = Bit.Core.NotificationCenter.Entities.Notification;
namespace Bit.Core.NotificationHub;
@@ -50,7 +53,7 @@ public class NotificationHubPushNotificationService : IPushNotificationService
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
}
- private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable collectionIds)
+ private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable? collectionIds)
{
if (cipher.OrganizationId.HasValue)
{
@@ -135,11 +138,7 @@ public class NotificationHubPushNotificationService : IPushNotificationService
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
{
- var message = new UserPushNotification
- {
- UserId = userId,
- Date = DateTime.UtcNow
- };
+ var message = new UserPushNotification { UserId = userId, Date = DateTime.UtcNow };
await SendPayloadToUserAsync(userId, type, message, excludeCurrentContext);
}
@@ -184,31 +183,89 @@ public class NotificationHubPushNotificationService : IPushNotificationService
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
}
+ public async Task PushNotificationAsync(Notification notification)
+ {
+ var message = new NotificationPushNotification
+ {
+ Id = notification.Id,
+ Priority = notification.Priority,
+ Global = notification.Global,
+ ClientType = notification.ClientType,
+ UserId = notification.UserId,
+ OrganizationId = notification.OrganizationId,
+ Title = notification.Title,
+ Body = notification.Body,
+ CreationDate = notification.CreationDate,
+ RevisionDate = notification.RevisionDate
+ };
+
+ if (notification.UserId.HasValue)
+ {
+ await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotification, message, true,
+ notification.ClientType);
+ }
+ else if (notification.OrganizationId.HasValue)
+ {
+ await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotification, message,
+ true, notification.ClientType);
+ }
+ }
+
+ public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
+ {
+ var message = new NotificationPushNotification
+ {
+ Id = notification.Id,
+ Priority = notification.Priority,
+ Global = notification.Global,
+ ClientType = notification.ClientType,
+ UserId = notification.UserId,
+ OrganizationId = notification.OrganizationId,
+ Title = notification.Title,
+ Body = notification.Body,
+ CreationDate = notification.CreationDate,
+ RevisionDate = notification.RevisionDate,
+ ReadDate = notificationStatus.ReadDate,
+ DeletedDate = notificationStatus.DeletedDate
+ };
+
+ if (notification.UserId.HasValue)
+ {
+ await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotificationStatus, message, true,
+ notification.ClientType);
+ }
+ else if (notification.OrganizationId.HasValue)
+ {
+ await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotificationStatus, message,
+ true, notification.ClientType);
+ }
+ }
+
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
{
- var message = new AuthRequestPushNotification
- {
- Id = authRequest.Id,
- UserId = authRequest.UserId
- };
+ var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId };
await SendPayloadToUserAsync(authRequest.UserId, type, message, true);
}
- private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext)
+ private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext,
+ ClientType? clientType = null)
{
- await SendPayloadToUserAsync(userId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext));
+ await SendPayloadToUserAsync(userId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext),
+ clientType: clientType);
}
- private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload, bool excludeCurrentContext)
+ private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload,
+ bool excludeCurrentContext, ClientType? clientType = null)
{
- await SendPayloadToUserAsync(orgId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext));
+ await SendPayloadToOrganizationAsync(orgId.ToString(), type, payload,
+ GetContextIdentifier(excludeCurrentContext), clientType: clientType);
}
- public async Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
- string deviceId = null)
+ public async Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
+ string? deviceId = null, ClientType? clientType = null)
{
- var tag = BuildTag($"template:payload_userId:{SanitizeTagInput(userId)}", identifier);
+ var tag = BuildTag($"template:payload_userId:{SanitizeTagInput(userId)}", identifier, clientType);
await SendPayloadAsync(tag, type, payload);
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
{
@@ -216,10 +273,10 @@ public class NotificationHubPushNotificationService : IPushNotificationService
}
}
- public async Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
- string deviceId = null)
+ public async Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
+ string? deviceId = null, ClientType? clientType = null)
{
- var tag = BuildTag($"template:payload && organizationId:{SanitizeTagInput(orgId)}", identifier);
+ var tag = BuildTag($"template:payload && organizationId:{SanitizeTagInput(orgId)}", identifier, clientType);
await SendPayloadAsync(tag, type, payload);
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
{
@@ -246,30 +303,36 @@ public class NotificationHubPushNotificationService : IPushNotificationService
{
OrganizationId = organization.Id,
LimitCollectionCreation = organization.LimitCollectionCreation,
- LimitCollectionDeletion = organization.LimitCollectionDeletion
+ LimitCollectionDeletion = organization.LimitCollectionDeletion,
+ LimitItemDeletion = organization.LimitItemDeletion
},
false
);
- private string GetContextIdentifier(bool excludeCurrentContext)
+ private string? GetContextIdentifier(bool excludeCurrentContext)
{
if (!excludeCurrentContext)
{
return null;
}
- var currentContext = _httpContextAccessor?.HttpContext?.
- RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
+ var currentContext =
+ _httpContextAccessor.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
return currentContext?.DeviceIdentifier;
}
- private string BuildTag(string tag, string identifier)
+ private string BuildTag(string tag, string? identifier, ClientType? clientType)
{
if (!string.IsNullOrWhiteSpace(identifier))
{
tag += $" && !deviceIdentifier:{SanitizeTagInput(identifier)}";
}
+ if (clientType.HasValue && clientType.Value != ClientType.All)
+ {
+ tag += $" && clientType:{clientType}";
+ }
+
return $"({tag})";
}
@@ -278,8 +341,7 @@ public class NotificationHubPushNotificationService : IPushNotificationService
var results = await _notificationHubPool.AllClients.SendTemplateNotificationAsync(
new Dictionary
{
- { "type", ((byte)type).ToString() },
- { "payload", JsonSerializer.Serialize(payload) }
+ { "type", ((byte)type).ToString() }, { "payload", JsonSerializer.Serialize(payload) }
}, tag);
if (_enableTracing)
@@ -290,7 +352,9 @@ public class NotificationHubPushNotificationService : IPushNotificationService
{
continue;
}
- _logger.LogInformation("Azure Notification Hub Tracking ID: {Id} | {Type} push notification with {Success} successes and {Failure} failures with a payload of {@Payload} and result of {@Results}",
+
+ _logger.LogInformation(
+ "Azure Notification Hub Tracking ID: {Id} | {Type} push notification with {Success} successes and {Failure} failures with a payload of {@Payload} and result of {@Results}",
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
}
}
diff --git a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs
index 180b2b641b..0c9bbea425 100644
--- a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs
+++ b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs
@@ -2,36 +2,26 @@
using Bit.Core.Models.Data;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
-using Bit.Core.Settings;
+using Bit.Core.Utilities;
using Microsoft.Azure.NotificationHubs;
-using Microsoft.Extensions.Logging;
namespace Bit.Core.NotificationHub;
public class NotificationHubPushRegistrationService : IPushRegistrationService
{
private readonly IInstallationDeviceRepository _installationDeviceRepository;
- private readonly GlobalSettings _globalSettings;
private readonly INotificationHubPool _notificationHubPool;
- private readonly IServiceProvider _serviceProvider;
- private readonly ILogger _logger;
public NotificationHubPushRegistrationService(
IInstallationDeviceRepository installationDeviceRepository,
- GlobalSettings globalSettings,
- INotificationHubPool notificationHubPool,
- IServiceProvider serviceProvider,
- ILogger logger)
+ INotificationHubPool notificationHubPool)
{
_installationDeviceRepository = installationDeviceRepository;
- _globalSettings = globalSettings;
_notificationHubPool = notificationHubPool;
- _serviceProvider = serviceProvider;
- _logger = logger;
}
public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
- string identifier, DeviceType type)
+ string identifier, DeviceType type, IEnumerable organizationIds)
{
if (string.IsNullOrWhiteSpace(pushToken))
{
@@ -45,16 +35,21 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
Templates = new Dictionary()
};
- installation.Tags = new List
- {
- $"userId:{userId}"
- };
+ var clientType = DeviceTypes.ToClientType(type);
+
+ installation.Tags = new List { $"userId:{userId}", $"clientType:{clientType}" };
if (!string.IsNullOrWhiteSpace(identifier))
{
installation.Tags.Add("deviceIdentifier:" + identifier);
}
+ var organizationIdsList = organizationIds.ToList();
+ foreach (var organizationId in organizationIdsList)
+ {
+ installation.Tags.Add($"organizationId:{organizationId}");
+ }
+
string payloadTemplate = null, messageTemplate = null, badgeMessageTemplate = null;
switch (type)
{
@@ -84,10 +79,12 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
break;
}
- BuildInstallationTemplate(installation, "payload", payloadTemplate, userId, identifier);
- BuildInstallationTemplate(installation, "message", messageTemplate, userId, identifier);
+ BuildInstallationTemplate(installation, "payload", payloadTemplate, userId, identifier, clientType,
+ organizationIdsList);
+ BuildInstallationTemplate(installation, "message", messageTemplate, userId, identifier, clientType,
+ organizationIdsList);
BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate,
- userId, identifier);
+ userId, identifier, clientType, organizationIdsList);
await ClientFor(GetComb(deviceId)).CreateOrUpdateInstallationAsync(installation);
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
@@ -97,7 +94,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
}
private void BuildInstallationTemplate(Installation installation, string templateId, string templateBody,
- string userId, string identifier)
+ string userId, string identifier, ClientType clientType, List organizationIds)
{
if (templateBody == null)
{
@@ -111,8 +108,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
Body = templateBody,
Tags = new List
{
- fullTemplateId,
- $"{fullTemplateId}_userId:{userId}"
+ fullTemplateId, $"{fullTemplateId}_userId:{userId}", $"clientType:{clientType}"
}
};
@@ -121,6 +117,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
template.Tags.Add($"{fullTemplateId}_deviceIdentifier:{identifier}");
}
+ foreach (var organizationId in organizationIds)
+ {
+ template.Tags.Add($"organizationId:{organizationId}");
+ }
+
installation.Templates.Add(fullTemplateId, template);
}
@@ -197,7 +198,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
}
}
- private NotificationHubClient ClientFor(Guid deviceId)
+ private INotificationHubClient ClientFor(Guid deviceId)
{
return _notificationHubPool.ClientFor(deviceId);
}
diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs
index 5586273520..7db514887c 100644
--- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs
+++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs
@@ -9,6 +9,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfa
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
+using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
@@ -52,6 +53,8 @@ public static class OrganizationServiceCollectionExtensions
services.AddOrganizationLicenseCommandsQueries();
services.AddOrganizationDomainCommandsQueries();
services.AddOrganizationSignUpCommands();
+ services.AddOrganizationDeleteCommands();
+ services.AddOrganizationEnableCommands();
services.AddOrganizationAuthCommands();
services.AddOrganizationUserCommands();
services.AddOrganizationUserCommandsQueries();
@@ -61,6 +64,15 @@ public static class OrganizationServiceCollectionExtensions
private static IServiceCollection AddOrganizationSignUpCommands(this IServiceCollection services) =>
services.AddScoped();
+ private static void AddOrganizationDeleteCommands(this IServiceCollection services)
+ {
+ services.AddScoped();
+ services.AddScoped();
+ }
+
+ private static void AddOrganizationEnableCommands(this IServiceCollection services) =>
+ services.AddScoped();
+
private static void AddOrganizationConnectionCommands(this IServiceCollection services)
{
services.AddScoped();
diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs
index 7f463460dd..19af8121e7 100644
--- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs
+++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs
@@ -224,27 +224,8 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{
- if (_featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
- {
- var sale = OrganizationSale.From(organization, upgrade);
- await _organizationBillingService.Finalize(sale);
- }
- else
- {
- try
- {
- paymentIntentClientSecret = await _paymentService.UpgradeFreeOrganizationAsync(organization,
- newPlan, upgrade);
- success = string.IsNullOrWhiteSpace(paymentIntentClientSecret);
- }
- catch
- {
- await _paymentService.CancelAndRecoverChargesAsync(organization);
- organization.GatewayCustomerId = null;
- await _organizationService.ReplaceAndUpdateCacheAsync(organization);
- throw;
- }
- }
+ var sale = OrganizationSale.From(organization, upgrade);
+ await _organizationBillingService.Finalize(sale);
}
else
{
diff --git a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs b/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs
index 0503a22a60..c32212c6b2 100644
--- a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs
+++ b/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs
@@ -1,30 +1,30 @@
-using System.Text.Json;
+#nullable enable
+using System.Text.Json;
using Azure.Storage.Queues;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Models;
-using Bit.Core.Settings;
+using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Tools.Entities;
using Bit.Core.Utilities;
using Bit.Core.Vault.Entities;
using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.Platform.Push.Internal;
public class AzureQueuePushNotificationService : IPushNotificationService
{
private readonly QueueClient _queueClient;
- private readonly GlobalSettings _globalSettings;
private readonly IHttpContextAccessor _httpContextAccessor;
public AzureQueuePushNotificationService(
- GlobalSettings globalSettings,
+ [FromKeyedServices("notifications")] QueueClient queueClient,
IHttpContextAccessor httpContextAccessor)
{
- _queueClient = new QueueClient(globalSettings.Notifications.ConnectionString, "notifications");
- _globalSettings = globalSettings;
+ _queueClient = queueClient;
_httpContextAccessor = httpContextAccessor;
}
@@ -43,7 +43,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
}
- private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable collectionIds)
+ private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable? collectionIds)
{
if (cipher.OrganizationId.HasValue)
{
@@ -129,11 +129,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
{
- var message = new UserPushNotification
- {
- UserId = userId,
- Date = DateTime.UtcNow
- };
+ var message = new UserPushNotification { UserId = userId, Date = DateTime.UtcNow };
await SendMessageAsync(type, message, excludeCurrentContext);
}
@@ -150,11 +146,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
{
- var message = new AuthRequestPushNotification
- {
- Id = authRequest.Id,
- UserId = authRequest.UserId
- };
+ var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId };
await SendMessageAsync(type, message, true);
}
@@ -174,6 +166,46 @@ public class AzureQueuePushNotificationService : IPushNotificationService
await PushSendAsync(send, PushType.SyncSendDelete);
}
+ public async Task PushNotificationAsync(Notification notification)
+ {
+ var message = new NotificationPushNotification
+ {
+ Id = notification.Id,
+ Priority = notification.Priority,
+ Global = notification.Global,
+ ClientType = notification.ClientType,
+ UserId = notification.UserId,
+ OrganizationId = notification.OrganizationId,
+ Title = notification.Title,
+ Body = notification.Body,
+ CreationDate = notification.CreationDate,
+ RevisionDate = notification.RevisionDate
+ };
+
+ await SendMessageAsync(PushType.SyncNotification, message, true);
+ }
+
+ public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
+ {
+ var message = new NotificationPushNotification
+ {
+ Id = notification.Id,
+ Priority = notification.Priority,
+ Global = notification.Global,
+ ClientType = notification.ClientType,
+ UserId = notification.UserId,
+ OrganizationId = notification.OrganizationId,
+ Title = notification.Title,
+ Body = notification.Body,
+ CreationDate = notification.CreationDate,
+ RevisionDate = notification.RevisionDate,
+ ReadDate = notificationStatus.ReadDate,
+ DeletedDate = notificationStatus.DeletedDate
+ };
+
+ await SendMessageAsync(PushType.SyncNotificationStatus, message, true);
+ }
+
private async Task PushSendAsync(Send send, PushType type)
{
if (send.UserId.HasValue)
@@ -197,27 +229,27 @@ public class AzureQueuePushNotificationService : IPushNotificationService
await _queueClient.SendMessageAsync(message);
}
- private string GetContextIdentifier(bool excludeCurrentContext)
+ private string? GetContextIdentifier(bool excludeCurrentContext)
{
if (!excludeCurrentContext)
{
return null;
}
- var currentContext = _httpContextAccessor?.HttpContext?.
- RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
+ var currentContext =
+ _httpContextAccessor?.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
return currentContext?.DeviceIdentifier;
}
- public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
- string deviceId = null)
+ public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
+ string? deviceId = null, ClientType? clientType = null)
{
// Noop
return Task.FromResult(0);
}
- public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
- string deviceId = null)
+ public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
+ string? deviceId = null, ClientType? clientType = null)
{
// Noop
return Task.FromResult(0);
@@ -239,6 +271,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService
{
OrganizationId = organization.Id,
LimitCollectionCreation = organization.LimitCollectionCreation,
- LimitCollectionDeletion = organization.LimitCollectionDeletion
+ LimitCollectionDeletion = organization.LimitCollectionDeletion,
+ LimitItemDeletion = organization.LimitItemDeletion
}, false);
}
diff --git a/src/Core/Platform/Push/Services/IPushNotificationService.cs b/src/Core/Platform/Push/Services/IPushNotificationService.cs
index b015c17df2..1c7fdc659b 100644
--- a/src/Core/Platform/Push/Services/IPushNotificationService.cs
+++ b/src/Core/Platform/Push/Services/IPushNotificationService.cs
@@ -1,6 +1,8 @@
-using Bit.Core.AdminConsole.Entities;
+#nullable enable
+using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Enums;
+using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
@@ -23,11 +25,14 @@ public interface IPushNotificationService
Task PushSyncSendCreateAsync(Send send);
Task PushSyncSendUpdateAsync(Send send);
Task PushSyncSendDeleteAsync(Send send);
+ Task PushNotificationAsync(Notification notification);
+ Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus);
Task PushAuthRequestAsync(AuthRequest authRequest);
Task PushAuthRequestResponseAsync(AuthRequest authRequest);
- Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, string deviceId = null);
- Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
- string deviceId = null);
Task PushSyncOrganizationStatusAsync(Organization organization);
Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization);
+ Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
+ string? deviceId = null, ClientType? clientType = null);
+ Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
+ string? deviceId = null, ClientType? clientType = null);
}
diff --git a/src/Core/Platform/Push/Services/IPushRegistrationService.cs b/src/Core/Platform/Push/Services/IPushRegistrationService.cs
index 482e7ae1c4..0c4271f061 100644
--- a/src/Core/Platform/Push/Services/IPushRegistrationService.cs
+++ b/src/Core/Platform/Push/Services/IPushRegistrationService.cs
@@ -5,7 +5,7 @@ namespace Bit.Core.Platform.Push;
public interface IPushRegistrationService
{
Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
- string identifier, DeviceType type);
+ string identifier, DeviceType type, IEnumerable organizationIds);
Task DeleteRegistrationAsync(string deviceId);
Task AddUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId);
Task DeleteUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId);
diff --git a/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs b/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs
index f1a5700013..9b4e66ae1a 100644
--- a/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs
+++ b/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs
@@ -1,6 +1,8 @@
-using Bit.Core.AdminConsole.Entities;
+#nullable enable
+using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Enums;
+using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
@@ -23,7 +25,7 @@ public class MultiServicePushNotificationService : IPushNotificationService
_logger = logger;
_logger.LogInformation("Hub services: {Services}", _services.Count());
- globalSettings?.NotificationHubPool?.NotificationHubs?.ForEach(hub =>
+ globalSettings.NotificationHubPool?.NotificationHubs?.ForEach(hub =>
{
_logger.LogInformation("HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}", hub.HubName, hub.EnableSendTracing, hub.RegistrationStartDate, hub.RegistrationEndDate);
});
@@ -131,20 +133,6 @@ public class MultiServicePushNotificationService : IPushNotificationService
return Task.FromResult(0);
}
- public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
- string deviceId = null)
- {
- PushToServices((s) => s.SendPayloadToUserAsync(userId, type, payload, identifier, deviceId));
- return Task.FromResult(0);
- }
-
- public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
- string deviceId = null)
- {
- PushToServices((s) => s.SendPayloadToOrganizationAsync(orgId, type, payload, identifier, deviceId));
- return Task.FromResult(0);
- }
-
public Task PushSyncOrganizationStatusAsync(Organization organization)
{
PushToServices((s) => s.PushSyncOrganizationStatusAsync(organization));
@@ -157,14 +145,44 @@ public class MultiServicePushNotificationService : IPushNotificationService
return Task.CompletedTask;
}
+ public Task PushNotificationAsync(Notification notification)
+ {
+ PushToServices((s) => s.PushNotificationAsync(notification));
+ return Task.CompletedTask;
+ }
+
+ public Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
+ {
+ PushToServices((s) => s.PushNotificationStatusAsync(notification, notificationStatus));
+ return Task.CompletedTask;
+ }
+
+ public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
+ string? deviceId = null, ClientType? clientType = null)
+ {
+ PushToServices((s) => s.SendPayloadToUserAsync(userId, type, payload, identifier, deviceId, clientType));
+ return Task.FromResult(0);
+ }
+
+ public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
+ string? deviceId = null, ClientType? clientType = null)
+ {
+ PushToServices((s) => s.SendPayloadToOrganizationAsync(orgId, type, payload, identifier, deviceId, clientType));
+ return Task.FromResult(0);
+ }
+
private void PushToServices(Func pushFunc)
{
- if (_services != null)
+ if (!_services.Any())
{
- foreach (var service in _services)
- {
- pushFunc(service);
- }
+ _logger.LogWarning("No services found to push notification");
+ return;
+ }
+
+ foreach (var service in _services)
+ {
+ _logger.LogDebug("Pushing notification to service {ServiceName}", service.GetType().Name);
+ pushFunc(service);
}
}
}
diff --git a/src/Core/Platform/Push/Services/NoopPushNotificationService.cs b/src/Core/Platform/Push/Services/NoopPushNotificationService.cs
index 4a185bee1a..57c446c5e5 100644
--- a/src/Core/Platform/Push/Services/NoopPushNotificationService.cs
+++ b/src/Core/Platform/Push/Services/NoopPushNotificationService.cs
@@ -1,6 +1,8 @@
-using Bit.Core.AdminConsole.Entities;
+#nullable enable
+using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Enums;
+using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
@@ -83,8 +85,8 @@ public class NoopPushNotificationService : IPushNotificationService
return Task.FromResult(0);
}
- public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
- string deviceId = null)
+ public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
+ string? deviceId = null, ClientType? clientType = null)
{
return Task.FromResult(0);
}
@@ -106,9 +108,14 @@ public class NoopPushNotificationService : IPushNotificationService
return Task.FromResult(0);
}
- public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
- string deviceId = null)
+ public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
+ string? deviceId = null, ClientType? clientType = null)
{
return Task.FromResult(0);
}
+
+ public Task PushNotificationAsync(Notification notification) => Task.CompletedTask;
+
+ public Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) =>
+ Task.CompletedTask;
}
diff --git a/src/Core/Platform/Push/Services/NoopPushRegistrationService.cs b/src/Core/Platform/Push/Services/NoopPushRegistrationService.cs
index 6d1716a6ce..6bcf9e893a 100644
--- a/src/Core/Platform/Push/Services/NoopPushRegistrationService.cs
+++ b/src/Core/Platform/Push/Services/NoopPushRegistrationService.cs
@@ -10,7 +10,7 @@ public class NoopPushRegistrationService : IPushRegistrationService
}
public Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
- string identifier, DeviceType type)
+ string identifier, DeviceType type, IEnumerable organizationIds)
{
return Task.FromResult(0);
}
diff --git a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs b/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs
index 849ae1b765..7a557e8978 100644
--- a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs
+++ b/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs
@@ -1,8 +1,10 @@
-using Bit.Core.AdminConsole.Entities;
+#nullable enable
+using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Models;
+using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
@@ -15,7 +17,6 @@ namespace Bit.Core.Platform.Push;
public class NotificationsApiPushNotificationService : BaseIdentityClientService, IPushNotificationService
{
- private readonly GlobalSettings _globalSettings;
private readonly IHttpContextAccessor _httpContextAccessor;
public NotificationsApiPushNotificationService(
@@ -32,7 +33,6 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
globalSettings.InternalIdentityKey,
logger)
{
- _globalSettings = globalSettings;
_httpContextAccessor = httpContextAccessor;
}
@@ -51,7 +51,7 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
}
- private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable collectionIds)
+ private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable? collectionIds)
{
if (cipher.OrganizationId.HasValue)
{
@@ -183,6 +183,46 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
await PushSendAsync(send, PushType.SyncSendDelete);
}
+ public async Task PushNotificationAsync(Notification notification)
+ {
+ var message = new NotificationPushNotification
+ {
+ Id = notification.Id,
+ Priority = notification.Priority,
+ Global = notification.Global,
+ ClientType = notification.ClientType,
+ UserId = notification.UserId,
+ OrganizationId = notification.OrganizationId,
+ Title = notification.Title,
+ Body = notification.Body,
+ CreationDate = notification.CreationDate,
+ RevisionDate = notification.RevisionDate
+ };
+
+ await SendMessageAsync(PushType.SyncNotification, message, true);
+ }
+
+ public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
+ {
+ var message = new NotificationPushNotification
+ {
+ Id = notification.Id,
+ Priority = notification.Priority,
+ Global = notification.Global,
+ ClientType = notification.ClientType,
+ UserId = notification.UserId,
+ OrganizationId = notification.OrganizationId,
+ Title = notification.Title,
+ Body = notification.Body,
+ CreationDate = notification.CreationDate,
+ RevisionDate = notification.RevisionDate,
+ ReadDate = notificationStatus.ReadDate,
+ DeletedDate = notificationStatus.DeletedDate
+ };
+
+ await SendMessageAsync(PushType.SyncNotificationStatus, message, true);
+ }
+
private async Task PushSendAsync(Send send, PushType type)
{
if (send.UserId.HasValue)
@@ -205,27 +245,27 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
await SendAsync(HttpMethod.Post, "send", request);
}
- private string GetContextIdentifier(bool excludeCurrentContext)
+ private string? GetContextIdentifier(bool excludeCurrentContext)
{
if (!excludeCurrentContext)
{
return null;
}
- var currentContext = _httpContextAccessor?.HttpContext?.
- RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
+ var currentContext =
+ _httpContextAccessor.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
return currentContext?.DeviceIdentifier;
}
- public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
- string deviceId = null)
+ public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
+ string? deviceId = null, ClientType? clientType = null)
{
// Noop
return Task.FromResult(0);
}
- public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
- string deviceId = null)
+ public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
+ string? deviceId = null, ClientType? clientType = null)
{
// Noop
return Task.FromResult(0);
@@ -248,6 +288,7 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
{
OrganizationId = organization.Id,
LimitCollectionCreation = organization.LimitCollectionCreation,
- LimitCollectionDeletion = organization.LimitCollectionDeletion
+ LimitCollectionDeletion = organization.LimitCollectionDeletion,
+ LimitItemDeletion = organization.LimitItemDeletion
}, false);
}
diff --git a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs b/src/Core/Platform/Push/Services/RelayPushNotificationService.cs
index e41244a1b8..09f42fd0d1 100644
--- a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs
+++ b/src/Core/Platform/Push/Services/RelayPushNotificationService.cs
@@ -1,10 +1,12 @@
-using Bit.Core.AdminConsole.Entities;
+#nullable enable
+using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.IdentityServer;
using Bit.Core.Models;
using Bit.Core.Models.Api;
+using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@@ -54,7 +56,7 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
}
- private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable collectionIds)
+ private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable? collectionIds)
{
if (cipher.OrganizationId.HasValue)
{
@@ -138,11 +140,7 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
{
- var message = new UserPushNotification
- {
- UserId = userId,
- Date = DateTime.UtcNow
- };
+ var message = new UserPushNotification { UserId = userId, Date = DateTime.UtcNow };
await SendPayloadToUserAsync(userId, type, message, excludeCurrentContext);
}
@@ -189,69 +187,67 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
{
- var message = new AuthRequestPushNotification
- {
- Id = authRequest.Id,
- UserId = authRequest.UserId
- };
+ var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId };
await SendPayloadToUserAsync(authRequest.UserId, type, message, true);
}
- private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext)
+ public async Task PushNotificationAsync(Notification notification)
{
- var request = new PushSendRequestModel
+ var message = new NotificationPushNotification
{
- UserId = userId.ToString(),
- Type = type,
- Payload = payload
+ Id = notification.Id,
+ Priority = notification.Priority,
+ Global = notification.Global,
+ ClientType = notification.ClientType,
+ UserId = notification.UserId,
+ OrganizationId = notification.OrganizationId,
+ Title = notification.Title,
+ Body = notification.Body,
+ CreationDate = notification.CreationDate,
+ RevisionDate = notification.RevisionDate
};
- await AddCurrentContextAsync(request, excludeCurrentContext);
- await SendAsync(HttpMethod.Post, "push/send", request);
- }
-
- private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload, bool excludeCurrentContext)
- {
- var request = new PushSendRequestModel
+ if (notification.UserId.HasValue)
{
- OrganizationId = orgId.ToString(),
- Type = type,
- Payload = payload
- };
-
- await AddCurrentContextAsync(request, excludeCurrentContext);
- await SendAsync(HttpMethod.Post, "push/send", request);
- }
-
- private async Task AddCurrentContextAsync(PushSendRequestModel request, bool addIdentifier)
- {
- var currentContext = _httpContextAccessor?.HttpContext?.
- RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
- if (!string.IsNullOrWhiteSpace(currentContext?.DeviceIdentifier))
+ await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotification, message, true,
+ notification.ClientType);
+ }
+ else if (notification.OrganizationId.HasValue)
{
- var device = await _deviceRepository.GetByIdentifierAsync(currentContext.DeviceIdentifier);
- if (device != null)
- {
- request.DeviceId = device.Id.ToString();
- }
- if (addIdentifier)
- {
- request.Identifier = currentContext.DeviceIdentifier;
- }
+ await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotification, message,
+ true, notification.ClientType);
}
}
- public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
- string deviceId = null)
+ public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
{
- throw new NotImplementedException();
- }
+ var message = new NotificationPushNotification
+ {
+ Id = notification.Id,
+ Priority = notification.Priority,
+ Global = notification.Global,
+ ClientType = notification.ClientType,
+ UserId = notification.UserId,
+ OrganizationId = notification.OrganizationId,
+ Title = notification.Title,
+ Body = notification.Body,
+ CreationDate = notification.CreationDate,
+ RevisionDate = notification.RevisionDate,
+ ReadDate = notificationStatus.ReadDate,
+ DeletedDate = notificationStatus.DeletedDate
+ };
- public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
- string deviceId = null)
- {
- throw new NotImplementedException();
+ if (notification.UserId.HasValue)
+ {
+ await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotificationStatus, message, true,
+ notification.ClientType);
+ }
+ else if (notification.OrganizationId.HasValue)
+ {
+ await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotificationStatus, message,
+ true, notification.ClientType);
+ }
}
public async Task PushSyncOrganizationStatusAsync(Organization organization)
@@ -273,8 +269,70 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
{
OrganizationId = organization.Id,
LimitCollectionCreation = organization.LimitCollectionCreation,
- LimitCollectionDeletion = organization.LimitCollectionDeletion
+ LimitCollectionDeletion = organization.LimitCollectionDeletion,
+ LimitItemDeletion = organization.LimitItemDeletion
},
false
);
+
+ private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext,
+ ClientType? clientType = null)
+ {
+ var request = new PushSendRequestModel
+ {
+ UserId = userId.ToString(),
+ Type = type,
+ Payload = payload,
+ ClientType = clientType
+ };
+
+ await AddCurrentContextAsync(request, excludeCurrentContext);
+ await SendAsync(HttpMethod.Post, "push/send", request);
+ }
+
+ private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload,
+ bool excludeCurrentContext, ClientType? clientType = null)
+ {
+ var request = new PushSendRequestModel
+ {
+ OrganizationId = orgId.ToString(),
+ Type = type,
+ Payload = payload,
+ ClientType = clientType
+ };
+
+ await AddCurrentContextAsync(request, excludeCurrentContext);
+ await SendAsync(HttpMethod.Post, "push/send", request);
+ }
+
+ private async Task AddCurrentContextAsync(PushSendRequestModel request, bool addIdentifier)
+ {
+ var currentContext =
+ _httpContextAccessor.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
+ if (!string.IsNullOrWhiteSpace(currentContext?.DeviceIdentifier))
+ {
+ var device = await _deviceRepository.GetByIdentifierAsync(currentContext.DeviceIdentifier);
+ if (device != null)
+ {
+ request.DeviceId = device.Id.ToString();
+ }
+
+ if (addIdentifier)
+ {
+ request.Identifier = currentContext.DeviceIdentifier;
+ }
+ }
+ }
+
+ public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
+ string? deviceId = null, ClientType? clientType = null)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
+ string? deviceId = null, ClientType? clientType = null)
+ {
+ throw new NotImplementedException();
+ }
}
diff --git a/src/Core/Platform/Push/Services/RelayPushRegistrationService.cs b/src/Core/Platform/Push/Services/RelayPushRegistrationService.cs
index 79b033e877..b838fbde59 100644
--- a/src/Core/Platform/Push/Services/RelayPushRegistrationService.cs
+++ b/src/Core/Platform/Push/Services/RelayPushRegistrationService.cs
@@ -25,7 +25,7 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi
}
public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
- string identifier, DeviceType type)
+ string identifier, DeviceType type, IEnumerable organizationIds)
{
var requestModel = new PushRegistrationRequestModel
{
@@ -33,7 +33,8 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi
Identifier = identifier,
PushToken = pushToken,
Type = type,
- UserId = userId
+ UserId = userId,
+ OrganizationIds = organizationIds
};
await SendAsync(HttpMethod.Post, "push/register", requestModel);
}
diff --git a/src/Core/Repositories/ITaxRateRepository.cs b/src/Core/Repositories/ITaxRateRepository.cs
deleted file mode 100644
index c4d9e41238..0000000000
--- a/src/Core/Repositories/ITaxRateRepository.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using Bit.Core.Entities;
-
-#nullable enable
-
-namespace Bit.Core.Repositories;
-
-public interface ITaxRateRepository : IRepository
-{
- Task> SearchAsync(int skip, int count);
- Task> GetAllActiveAsync();
- Task ArchiveAsync(TaxRate model);
- Task> GetByLocationAsync(TaxRate taxRate);
-}
diff --git a/src/Core/Services/ICollectionService.cs b/src/Core/Services/ICollectionService.cs
index 27c4118197..c116e5f076 100644
--- a/src/Core/Services/ICollectionService.cs
+++ b/src/Core/Services/ICollectionService.cs
@@ -7,6 +7,4 @@ public interface ICollectionService
{
Task SaveAsync(Collection collection, IEnumerable groups = null, IEnumerable users = null);
Task DeleteUserAsync(Collection collection, Guid organizationUserId);
- [Obsolete("Pre-Flexible Collections logic.")]
- Task> GetOrganizationCollectionsAsync(Guid organizationId);
}
diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs
index 0f69d8daaf..92d05ddb7d 100644
--- a/src/Core/Services/IMailService.cs
+++ b/src/Core/Services/IMailService.cs
@@ -14,6 +14,7 @@ public interface IMailService
Task SendVerifyEmailEmailAsync(string email, Guid userId, string token);
Task SendRegistrationVerificationEmailAsync(string email, string token);
Task SendTrialInitiationSignupEmailAsync(
+ bool isExistingUser,
string email,
string token,
ProductTierType productTier,
@@ -36,7 +37,7 @@ public interface IMailService
Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable adminEmails, bool hasAccessSecretsManager = false);
Task SendOrganizationConfirmedEmailAsync(string organizationName, string email, bool hasAccessSecretsManager = false);
Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email);
- Task SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(string organizationName, string email);
+ Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email);
Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email);
Task SendPasswordlessSignInAsync(string returnUrl, string token, string email);
Task SendInvoiceUpcoming(
@@ -96,5 +97,6 @@ public interface IMailService
Task SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(string email, string offerAcceptanceDate, string organizationId,
string organizationName);
Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList);
+ Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable adminEmails, Guid organizationId, string email, string userName);
}
diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs
index 7d0f9d3c63..5bd2bede33 100644
--- a/src/Core/Services/IPaymentService.cs
+++ b/src/Core/Services/IPaymentService.cs
@@ -54,9 +54,6 @@ public interface IPaymentService
Task GetSubscriptionAsync(ISubscriber subscriber);
Task GetTaxInfoAsync(ISubscriber subscriber);
Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo);
- Task CreateTaxRateAsync(TaxRate taxRate);
- Task UpdateTaxRateAsync(TaxRate taxRate);
- Task ArchiveTaxRateAsync(TaxRate taxRate);
Task AddSecretsManagerToSubscription(Organization org, Plan plan, int additionalSmSeats,
int additionalServiceAccount);
Task RisksSubscriptionFailure(Organization organization);
diff --git a/src/Core/Services/IStripeAdapter.cs b/src/Core/Services/IStripeAdapter.cs
index ef2e3ab766..cb95732a6e 100644
--- a/src/Core/Services/IStripeAdapter.cs
+++ b/src/Core/Services/IStripeAdapter.cs
@@ -43,9 +43,6 @@ public interface IStripeAdapter
IAsyncEnumerable PaymentMethodListAutoPagingAsync(Stripe.PaymentMethodListOptions options);
Task PaymentMethodAttachAsync(string id, Stripe.PaymentMethodAttachOptions options = null);
Task PaymentMethodDetachAsync(string id, Stripe.PaymentMethodDetachOptions options = null);
- Task PlanGetAsync(string id, Stripe.PlanGetOptions options = null);
- Task TaxRateCreateAsync(Stripe.TaxRateCreateOptions options);
- Task TaxRateUpdateAsync(string id, Stripe.TaxRateUpdateOptions options);
Task TaxIdCreateAsync(string id, Stripe.TaxIdCreateOptions options);
Task TaxIdDeleteAsync(string customerId, string taxIdId, Stripe.TaxIdDeleteOptions options = null);
Task> ChargeListAsync(Stripe.ChargeListOptions options);
diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs
index 0886d18897..d1c61e4418 100644
--- a/src/Core/Services/IUserService.cs
+++ b/src/Core/Services/IUserService.cs
@@ -22,7 +22,6 @@ public interface IUserService
Task CreateUserAsync(User user, string masterPasswordHash);
Task SendMasterPasswordHintAsync(string email);
Task SendTwoFactorEmailAsync(User user);
- Task VerifyTwoFactorEmailAsync(User user, string token);
Task StartWebAuthnRegistrationAsync(User user);
Task DeleteWebAuthnKeyAsync(User user, int id);
Task CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse);
@@ -41,8 +40,6 @@ public interface IUserService
Task RefreshSecurityStampAsync(User user, string masterPasswordHash);
Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type, bool setEnabled = true, bool logEvent = true);
Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type);
- Task RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode);
- Task GenerateUserTokenAsync(User user, string tokenProvider, string purpose);
Task DeleteAsync(User user);
Task DeleteAsync(User user, string token);
Task SendDeleteConfirmationAsync(string email);
@@ -55,9 +52,7 @@ public interface IUserService
Task CancelPremiumAsync(User user, bool? endOfPeriod = null);
Task ReinstatePremiumAsync(User user);
Task EnablePremiumAsync(Guid userId, DateTime? expirationDate);
- Task EnablePremiumAsync(User user, DateTime? expirationDate);
Task DisablePremiumAsync(Guid userId, DateTime? expirationDate);
- Task DisablePremiumAsync(User user, DateTime? expirationDate);
Task UpdatePremiumExpirationAsync(Guid userId, DateTime? expirationDate);
Task GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null,
int? version = null);
@@ -91,9 +86,26 @@ public interface IUserService
void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool setEnabled = true);
+ [Obsolete("To be removed when the feature flag pm-17128-recovery-code-login is removed PM-18175.")]
+ Task RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode);
+
///
- /// Returns true if the user is a legacy user. Legacy users use their master key as their encryption key.
- /// We force these users to the web to migrate their encryption scheme.
+ /// This method is used by the TwoFactorAuthenticationValidator to recover two
+ /// factor for a user. This allows users to be logged in after a successful recovery
+ /// attempt.
+ ///
+ /// This method logs the event, sends an email to the user, and removes two factor
+ /// providers on the user account. This means that a user will have to accomplish
+ /// new device verification on their account on new logins, if it is enabled for their user.
+ ///
+ /// recovery code associated with the user logging in
+ /// The user to refresh the 2FA and Recovery Code on.
+ /// true if the recovery code is valid; false otherwise
+ Task RecoverTwoFactorAsync(User user, string recoveryCode);
+
+ ///
+ /// Returns true if the user is a legacy user. Legacy users use their master key as their
+ /// encryption key. We force these users to the web to migrate their encryption scheme.
///
Task IsLegacyUser(string userId);
@@ -101,7 +113,8 @@ public interface IUserService
/// Indicates if the user is managed by any organization.
///
///
- /// A user is considered managed by an organization if their email domain matches one of the verified domains of that organization, and the user is a member of it.
+ /// A user is considered managed by an organization if their email domain matches one of the
+ /// verified domains of that organization, and the user is a member of it.
/// The organization must be enabled and able to have verified domains.
///
///
diff --git a/src/Core/Services/Implementations/CollectionService.cs b/src/Core/Services/Implementations/CollectionService.cs
index e779ac289f..f6e9735f4e 100644
--- a/src/Core/Services/Implementations/CollectionService.cs
+++ b/src/Core/Services/Implementations/CollectionService.cs
@@ -95,31 +95,4 @@ public class CollectionService : ICollectionService
await _collectionRepository.DeleteUserAsync(collection.Id, organizationUserId);
await _eventService.LogOrganizationUserEventAsync(orgUser, Enums.EventType.OrganizationUser_Updated);
}
-
- public async Task> GetOrganizationCollectionsAsync(Guid organizationId)
- {
- if (
- !await _currentContext.ViewAllCollections(organizationId) &&
- !await _currentContext.ManageUsers(organizationId) &&
- !await _currentContext.ManageGroups(organizationId) &&
- !await _currentContext.AccessImportExport(organizationId)
- )
- {
- throw new NotFoundException();
- }
-
- IEnumerable orgCollections;
- if (await _currentContext.ViewAllCollections(organizationId) || await _currentContext.AccessImportExport(organizationId))
- {
- // Admins, Owners, Providers and Custom (with collection management or import/export permissions) can access all items even if not assigned to them
- orgCollections = await _collectionRepository.GetManyByOrganizationIdAsync(organizationId);
- }
- else
- {
- var collections = await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId.Value);
- orgCollections = collections.Where(c => c.OrganizationId == organizationId);
- }
-
- return orgCollections;
- }
}
diff --git a/src/Core/Services/Implementations/DeviceService.cs b/src/Core/Services/Implementations/DeviceService.cs
index afbc574417..28823eeda7 100644
--- a/src/Core/Services/Implementations/DeviceService.cs
+++ b/src/Core/Services/Implementations/DeviceService.cs
@@ -1,6 +1,7 @@
using Bit.Core.Auth.Models.Api.Request;
using Bit.Core.Auth.Utilities;
using Bit.Core.Entities;
+using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
@@ -11,13 +12,16 @@ public class DeviceService : IDeviceService
{
private readonly IDeviceRepository _deviceRepository;
private readonly IPushRegistrationService _pushRegistrationService;
+ private readonly IOrganizationUserRepository _organizationUserRepository;
public DeviceService(
IDeviceRepository deviceRepository,
- IPushRegistrationService pushRegistrationService)
+ IPushRegistrationService pushRegistrationService,
+ IOrganizationUserRepository organizationUserRepository)
{
_deviceRepository = deviceRepository;
_pushRegistrationService = pushRegistrationService;
+ _organizationUserRepository = organizationUserRepository;
}
public async Task SaveAsync(Device device)
@@ -32,8 +36,13 @@ public class DeviceService : IDeviceService
await _deviceRepository.ReplaceAsync(device);
}
+ var organizationIdsString =
+ (await _organizationUserRepository.GetManyDetailsByUserAsync(device.UserId,
+ OrganizationUserStatusType.Confirmed))
+ .Select(ou => ou.OrganizationId.ToString());
+
await _pushRegistrationService.CreateOrUpdateRegistrationAsync(device.PushToken, device.Id.ToString(),
- device.UserId.ToString(), device.Identifier, device.Type);
+ device.UserId.ToString(), device.Identifier, device.Type, organizationIdsString);
}
public async Task ClearTokenAsync(Device device)
diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs
index deae80c056..d18a29b13a 100644
--- a/src/Core/Services/Implementations/HandlebarsMailService.cs
+++ b/src/Core/Services/Implementations/HandlebarsMailService.cs
@@ -2,6 +2,7 @@
using System.Reflection;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
+using Bit.Core.AdminConsole.Models.Mail;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Mail;
using Bit.Core.Billing.Enums;
@@ -73,6 +74,7 @@ public class HandlebarsMailService : IMailService
}
public async Task SendTrialInitiationSignupEmailAsync(
+ bool isExistingUser,
string email,
string token,
ProductTierType productTier,
@@ -81,6 +83,7 @@ public class HandlebarsMailService : IMailService
var message = CreateDefaultMessage("Verify your email", email);
var model = new TrialInitiationVerifyEmail
{
+ IsExistingUser = isExistingUser,
Token = WebUtility.UrlEncode(token),
Email = WebUtility.UrlEncode(email),
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
@@ -295,7 +298,7 @@ public class HandlebarsMailService : IMailService
await _mailDeliveryService.SendEmailAsync(message);
}
- public async Task SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(string organizationName, string email)
+ public async Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email)
{
var message = CreateDefaultMessage($"You have been revoked from {organizationName}", email);
var model = new OrganizationUserRevokedForPolicyTwoFactorViewModel
@@ -472,7 +475,7 @@ public class HandlebarsMailService : IMailService
"AdminConsole.DomainClaimedByOrganization",
new ClaimedDomainUserNotificationViewModel
{
- TitleFirst = $"Hey {emailAddress}, your account is owned by {org.DisplayName()}",
+ TitleFirst = $"Your Bitwarden account is claimed by {org.DisplayName()}",
OrganizationName = CoreHelpers.SanitizeForEmail(org.DisplayName(), false)
});
}
@@ -863,7 +866,7 @@ public class HandlebarsMailService : IMailService
var message = CreateDefaultMessage($"Join {providerName}", email);
var model = new ProviderUserInvitedViewModel
{
- ProviderName = CoreHelpers.SanitizeForEmail(providerName),
+ ProviderName = CoreHelpers.SanitizeForEmail(providerName, false),
Email = WebUtility.UrlEncode(providerUser.Email),
ProviderId = providerUser.ProviderId.ToString(),
ProviderUserId = providerUser.Id.ToString(),
@@ -1168,6 +1171,23 @@ public class HandlebarsMailService : IMailService
await _mailDeliveryService.SendEmailAsync(message);
}
+ public async Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable adminEmails, Guid organizationId, string email, string userName)
+ {
+ var templateName = _globalSettings.SelfHosted ?
+ "AdminConsole.SelfHostNotifyAdminDeviceApprovalRequested" :
+ "AdminConsole.NotifyAdminDeviceApprovalRequested";
+ var message = CreateDefaultMessage("Review SSO login request for new device", adminEmails);
+ var model = new DeviceApprovalRequestedViewModel
+ {
+ WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
+ UserNameRequestingAccess = GetUserIdentifier(email, userName),
+ OrganizationId = organizationId,
+ };
+ await AddMessageContentAsync(message, templateName, model);
+ message.Category = "DeviceApprovalRequested";
+ await _mailDeliveryService.SendEmailAsync(message);
+ }
+
private static string GetUserIdentifier(string email, string userName)
{
return string.IsNullOrEmpty(userName) ? email : CoreHelpers.SanitizeForEmail(userName, false);
diff --git a/src/Core/Services/Implementations/StripeAdapter.cs b/src/Core/Services/Implementations/StripeAdapter.cs
index f4f8efe75f..f7f4fea066 100644
--- a/src/Core/Services/Implementations/StripeAdapter.cs
+++ b/src/Core/Services/Implementations/StripeAdapter.cs
@@ -9,7 +9,6 @@ public class StripeAdapter : IStripeAdapter
private readonly Stripe.SubscriptionService _subscriptionService;
private readonly Stripe.InvoiceService _invoiceService;
private readonly Stripe.PaymentMethodService _paymentMethodService;
- private readonly Stripe.TaxRateService _taxRateService;
private readonly Stripe.TaxIdService _taxIdService;
private readonly Stripe.ChargeService _chargeService;
private readonly Stripe.RefundService _refundService;
@@ -27,7 +26,6 @@ public class StripeAdapter : IStripeAdapter
_subscriptionService = new Stripe.SubscriptionService();
_invoiceService = new Stripe.InvoiceService();
_paymentMethodService = new Stripe.PaymentMethodService();
- _taxRateService = new Stripe.TaxRateService();
_taxIdService = new Stripe.TaxIdService();
_chargeService = new Stripe.ChargeService();
_refundService = new Stripe.RefundService();
@@ -196,16 +194,6 @@ public class StripeAdapter : IStripeAdapter
return _planService.GetAsync(id, options);
}
- public Task TaxRateCreateAsync(Stripe.TaxRateCreateOptions options)
- {
- return _taxRateService.CreateAsync(options);
- }
-
- public Task TaxRateUpdateAsync(string id, Stripe.TaxRateUpdateOptions options)
- {
- return _taxRateService.UpdateAsync(id, options);
- }
-
public Task TaxIdCreateAsync(string id, Stripe.TaxIdCreateOptions options)
{
return _taxIdService.CreateAsync(id, options);
diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs
index d6b711f366..5d80659876 100644
--- a/src/Core/Services/Implementations/StripePaymentService.cs
+++ b/src/Core/Services/Implementations/StripePaymentService.cs
@@ -19,7 +19,6 @@ using Microsoft.Extensions.Logging;
using Stripe;
using PaymentMethod = Stripe.PaymentMethod;
using StaticStore = Bit.Core.Models.StaticStore;
-using TaxRate = Bit.Core.Entities.TaxRate;
namespace Bit.Core.Services;
@@ -33,7 +32,6 @@ public class StripePaymentService : IPaymentService
private readonly ITransactionRepository _transactionRepository;
private readonly ILogger _logger;
private readonly Braintree.IBraintreeGateway _btGateway;
- private readonly ITaxRateRepository _taxRateRepository;
private readonly IStripeAdapter _stripeAdapter;
private readonly IGlobalSettings _globalSettings;
private readonly IFeatureService _featureService;
@@ -43,7 +41,6 @@ public class StripePaymentService : IPaymentService
public StripePaymentService(
ITransactionRepository transactionRepository,
ILogger logger,
- ITaxRateRepository taxRateRepository,
IStripeAdapter stripeAdapter,
Braintree.IBraintreeGateway braintreeGateway,
IGlobalSettings globalSettings,
@@ -53,7 +50,6 @@ public class StripePaymentService : IPaymentService
{
_transactionRepository = transactionRepository;
_logger = logger;
- _taxRateRepository = taxRateRepository;
_stripeAdapter = stripeAdapter;
_btGateway = braintreeGateway;
_globalSettings = globalSettings;
@@ -123,7 +119,7 @@ public class StripePaymentService : IPaymentService
Subscription subscription;
try
{
- if (taxInfo.TaxIdNumber != null && taxInfo.TaxIdType == null)
+ if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber))
{
taxInfo.TaxIdType = _taxService.GetStripeTaxCode(taxInfo.BillingAddressCountry,
taxInfo.TaxIdNumber);
@@ -171,7 +167,7 @@ public class StripePaymentService : IPaymentService
City = taxInfo?.BillingAddressCity,
State = taxInfo?.BillingAddressState,
},
- TaxIdData = taxInfo.HasTaxId
+ TaxIdData = !string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber)
? [new CustomerTaxIdDataOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber }]
: null
};
@@ -181,11 +177,7 @@ public class StripePaymentService : IPaymentService
customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions);
subCreateOptions.AddExpand("latest_invoice.payment_intent");
subCreateOptions.Customer = customer.Id;
-
- if (CustomerHasTaxLocationVerified(customer))
- {
- subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
- }
+ subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null)
@@ -366,13 +358,10 @@ public class StripePaymentService : IPaymentService
customer = await _stripeAdapter.CustomerUpdateAsync(org.GatewayCustomerId, customerUpdateOptions);
}
- var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade);
-
- if (CustomerHasTaxLocationVerified(customer))
+ var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade)
{
- subCreateOptions.DefaultTaxRates = [];
- subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
- }
+ AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
+ };
var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions);
@@ -531,6 +520,10 @@ public class StripePaymentService : IPaymentService
var customerCreateOptions = new CustomerCreateOptions
{
+ Tax = new CustomerTaxOptions
+ {
+ ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately
+ },
Description = user.Name,
Email = user.Email,
Metadata = stripeCustomerMetadata,
@@ -568,6 +561,7 @@ public class StripePaymentService : IPaymentService
var subCreateOptions = new SubscriptionCreateOptions
{
+ AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
Customer = customer.Id,
Items = [],
Metadata = new Dictionary
@@ -587,16 +581,10 @@ public class StripePaymentService : IPaymentService
subCreateOptions.Items.Add(new SubscriptionItemOptions
{
Plan = StoragePlanId,
- Quantity = additionalStorageGb
+ Quantity = additionalStorageGb,
});
}
- if (CustomerHasTaxLocationVerified(customer))
- {
- subCreateOptions.DefaultTaxRates = [];
- subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
- }
-
var subscription = await ChargeForNewSubscriptionAsync(user, customer, createdStripeCustomer,
stripePaymentMethod, paymentMethodType, subCreateOptions, braintreeCustomer);
@@ -634,10 +622,7 @@ public class StripePaymentService : IPaymentService
SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items)
});
- if (CustomerHasTaxLocationVerified(customer))
- {
- previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true };
- }
+ previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true };
if (previewInvoice.AmountDue > 0)
{
@@ -695,14 +680,12 @@ public class StripePaymentService : IPaymentService
Customer = customer.Id,
SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items),
SubscriptionDefaultTaxRates = subCreateOptions.DefaultTaxRates,
+ AutomaticTax = new InvoiceAutomaticTaxOptions
+ {
+ Enabled = true
+ }
};
- if (CustomerHasTaxLocationVerified(customer))
- {
- upcomingInvoiceOptions.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true };
- upcomingInvoiceOptions.SubscriptionDefaultTaxRates = [];
- }
-
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(upcomingInvoiceOptions);
if (previewInvoice.AmountDue > 0)
@@ -821,7 +804,11 @@ public class StripePaymentService : IPaymentService
Items = updatedItemOptions,
ProrationBehavior = invoiceNow ? Constants.AlwaysInvoice : Constants.CreateProrations,
DaysUntilDue = daysUntilDue ?? 1,
- CollectionMethod = "send_invoice"
+ CollectionMethod = "send_invoice",
+ AutomaticTax = new SubscriptionAutomaticTaxOptions
+ {
+ Enabled = true
+ }
};
if (!invoiceNow && isAnnualPlan && sub.Status.Trim() != "trialing")
{
@@ -829,13 +816,6 @@ public class StripePaymentService : IPaymentService
new SubscriptionPendingInvoiceItemIntervalOptions { Interval = "month" };
}
- if (sub.AutomaticTax.Enabled != true &&
- CustomerHasTaxLocationVerified(sub.Customer))
- {
- subUpdateOptions.DefaultTaxRates = [];
- subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
- }
-
if (!subscriptionUpdate.UpdateNeeded(sub))
{
// No need to update subscription, quantity matches
@@ -1520,13 +1500,11 @@ public class StripePaymentService : IPaymentService
if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId) &&
customer.Subscriptions.Any(sub =>
sub.Id == subscriber.GatewaySubscriptionId &&
- !sub.AutomaticTax.Enabled) &&
- CustomerHasTaxLocationVerified(customer))
+ !sub.AutomaticTax.Enabled))
{
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
{
- AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
- DefaultTaxRates = []
+ AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
};
_ = await _stripeAdapter.SubscriptionUpdateAsync(
@@ -1778,50 +1756,6 @@ public class StripePaymentService : IPaymentService
}
}
- public async Task CreateTaxRateAsync(TaxRate taxRate)
- {
- var stripeTaxRateOptions = new TaxRateCreateOptions()
- {
- DisplayName = $"{taxRate.Country} - {taxRate.PostalCode}",
- Inclusive = false,
- Percentage = taxRate.Rate,
- Active = true
- };
- var stripeTaxRate = await _stripeAdapter.TaxRateCreateAsync(stripeTaxRateOptions);
- taxRate.Id = stripeTaxRate.Id;
- await _taxRateRepository.CreateAsync(taxRate);
- return taxRate;
- }
-
- public async Task UpdateTaxRateAsync(TaxRate taxRate)
- {
- if (string.IsNullOrWhiteSpace(taxRate.Id))
- {
- return;
- }
-
- await ArchiveTaxRateAsync(taxRate);
- await CreateTaxRateAsync(taxRate);
- }
-
- public async Task ArchiveTaxRateAsync(TaxRate taxRate)
- {
- if (string.IsNullOrWhiteSpace(taxRate.Id))
- {
- return;
- }
-
- var updatedStripeTaxRate = await _stripeAdapter.TaxRateUpdateAsync(
- taxRate.Id,
- new TaxRateUpdateOptions() { Active = false }
- );
- if (!updatedStripeTaxRate.Active)
- {
- taxRate.Active = false;
- await _taxRateRepository.ArchiveAsync(taxRate);
- }
- }
-
public async Task AddSecretsManagerToSubscription(
Organization org,
StaticStore.Plan plan,
@@ -1918,7 +1852,6 @@ public class StripePaymentService : IPaymentService
Enabled = true,
},
Currency = "usd",
- Discounts = new List(),
SubscriptionDetails = new InvoiceSubscriptionDetailsOptions
{
Items =
@@ -1969,29 +1902,23 @@ public class StripePaymentService : IPaymentService
];
}
- if (gatewayCustomerId != null)
+ if (!string.IsNullOrWhiteSpace(gatewayCustomerId))
{
var gatewayCustomer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId);
if (gatewayCustomer.Discount != null)
{
- options.Discounts.Add(new InvoiceDiscountOptions
- {
- Discount = gatewayCustomer.Discount.Id
- });
+ options.Coupon = gatewayCustomer.Discount.Coupon.Id;
}
+ }
- if (gatewaySubscriptionId != null)
+ if (!string.IsNullOrWhiteSpace(gatewaySubscriptionId))
+ {
+ var gatewaySubscription = await _stripeAdapter.SubscriptionGetAsync(gatewaySubscriptionId);
+
+ if (gatewaySubscription?.Discount != null)
{
- var gatewaySubscription = await _stripeAdapter.SubscriptionGetAsync(gatewaySubscriptionId);
-
- if (gatewaySubscription?.Discount != null)
- {
- options.Discounts.Add(new InvoiceDiscountOptions
- {
- Discount = gatewaySubscription.Discount.Id
- });
- }
+ options.Coupon ??= gatewaySubscription.Discount.Coupon.Id;
}
}
@@ -2042,7 +1969,6 @@ public class StripePaymentService : IPaymentService
Enabled = true,
},
Currency = "usd",
- Discounts = new List(),
SubscriptionDetails = new InvoiceSubscriptionDetailsOptions
{
Items =
@@ -2106,7 +2032,7 @@ public class StripePaymentService : IPaymentService
}
}
- if (!string.IsNullOrEmpty(parameters.TaxInformation.TaxId))
+ if (!string.IsNullOrWhiteSpace(parameters.TaxInformation.TaxId))
{
var taxIdType = _taxService.GetStripeTaxCode(
options.CustomerDetails.Address.Country,
@@ -2129,26 +2055,23 @@ public class StripePaymentService : IPaymentService
];
}
- if (gatewayCustomerId != null)
+ if (!string.IsNullOrWhiteSpace(gatewayCustomerId))
{
var gatewayCustomer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId);
if (gatewayCustomer.Discount != null)
{
- options.Discounts.Add(new InvoiceDiscountOptions
- {
- Discount = gatewayCustomer.Discount.Id
- });
+ options.Coupon = gatewayCustomer.Discount.Coupon.Id;
}
+ }
+ if (!string.IsNullOrWhiteSpace(gatewaySubscriptionId))
+ {
var gatewaySubscription = await _stripeAdapter.SubscriptionGetAsync(gatewaySubscriptionId);
if (gatewaySubscription?.Discount != null)
{
- options.Discounts.Add(new InvoiceDiscountOptions
- {
- Discount = gatewaySubscription.Discount.Id
- });
+ options.Coupon ??= gatewaySubscription.Discount.Coupon.Id;
}
}
@@ -2328,14 +2251,6 @@ public class StripePaymentService : IPaymentService
}
}
- ///
- /// Determines if a Stripe customer supports automatic tax
- ///
- ///
- ///
- private static bool CustomerHasTaxLocationVerified(Customer customer) =>
- customer?.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported;
-
// We are taking only first 30 characters of the SubscriberName because stripe provide
// for 30 characters for custom_fields,see the link: https://stripe.com/docs/api/invoices/create
private static string GetFirstThirtyCharacters(string subscriberName)
diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs
index 4944dfe9e7..e04290a686 100644
--- a/src/Core/Services/Implementations/UserService.cs
+++ b/src/Core/Services/Implementations/UserService.cs
@@ -9,6 +9,7 @@ using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
+using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
@@ -314,7 +315,7 @@ public class UserService : UserManager, IUserService, IDisposable
return;
}
- var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "DeleteAccount");
+ var token = await GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "DeleteAccount");
await _mailService.SendVerifyDeleteEmailAsync(user.Email, user.Id, token);
}
@@ -867,6 +868,10 @@ public class UserService : UserManager, IUserService, IDisposable
}
}
+ ///
+ /// To be removed when the feature flag pm-17128-recovery-code-login is removed PM-18175.
+ ///
+ [Obsolete("Two Factor recovery is handled in the TwoFactorAuthenticationValidator.")]
public async Task RecoverTwoFactorAsync(string email, string secret, string recoveryCode)
{
var user = await _userRepository.GetByEmailAsync(email);
@@ -896,6 +901,25 @@ public class UserService : UserManager, IUserService, IDisposable
return true;
}
+ public async Task RecoverTwoFactorAsync(User user, string recoveryCode)
+ {
+ if (!CoreHelpers.FixedTimeEquals(
+ user.TwoFactorRecoveryCode,
+ recoveryCode.Replace(" ", string.Empty).Trim().ToLower()))
+ {
+ return false;
+ }
+
+ user.TwoFactorProviders = null;
+ user.TwoFactorRecoveryCode = CoreHelpers.SecureRandomString(32, upper: false, special: false);
+ await SaveUserAsync(user);
+ await _mailService.SendRecoverTwoFactorEmail(user.Email, DateTime.UtcNow, _currentContext.IpAddress);
+ await _eventService.LogUserEventAsync(user.Id, EventType.User_Recovered2fa);
+ await CheckPoliciesOnTwoFactorRemovalAsync(user);
+
+ return true;
+ }
+
public async Task> SignUpPremiumAsync(User user, string paymentToken,
PaymentMethodType paymentMethodType, short additionalStorageGb, UserLicense license,
TaxInfo taxInfo)
@@ -933,18 +957,8 @@ public class UserService : UserManager, IUserService, IDisposable
}
else
{
- var deprecateStripeSourcesAPI = _featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI);
-
- if (deprecateStripeSourcesAPI)
- {
- var sale = PremiumUserSale.From(user, paymentMethodType, paymentToken, taxInfo, additionalStorageGb);
- await _premiumUserBillingService.Finalize(sale);
- }
- else
- {
- paymentIntentClientSecret = await _paymentService.PurchasePremiumAsync(user, paymentMethodType,
- paymentToken, additionalStorageGb, taxInfo);
- }
+ var sale = PremiumUserSale.From(user, paymentMethodType, paymentToken, taxInfo, additionalStorageGb);
+ await _premiumUserBillingService.Finalize(sale);
}
user.Premium = true;
@@ -1054,11 +1068,11 @@ public class UserService : UserManager, IUserService, IDisposable
throw new BadRequestException("Invalid token.");
}
- var updated = await _paymentService.UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken, taxInfo: taxInfo);
- if (updated)
- {
- await SaveUserAsync(user);
- }
+ var tokenizedPaymentSource = new TokenizedPaymentSource(paymentMethodType, paymentToken);
+ var taxInformation = TaxInformation.From(taxInfo);
+
+ await _premiumUserBillingService.UpdatePaymentMethod(user, tokenizedPaymentSource, taxInformation);
+ await SaveUserAsync(user);
}
public async Task CancelPremiumAsync(User user, bool? endOfPeriod = null)
@@ -1090,7 +1104,7 @@ public class UserService : UserManager, IUserService, IDisposable
await EnablePremiumAsync(user, expirationDate);
}
- public async Task EnablePremiumAsync(User user, DateTime? expirationDate)
+ private async Task EnablePremiumAsync(User user, DateTime? expirationDate)
{
if (user != null && !user.Premium && user.Gateway.HasValue)
{
@@ -1107,7 +1121,7 @@ public class UserService : UserManager, IUserService, IDisposable
await DisablePremiumAsync(user, expirationDate);
}
- public async Task DisablePremiumAsync(User user, DateTime? expirationDate)
+ private async Task DisablePremiumAsync(User user, DateTime? expirationDate)
{
if (user != null && user.Premium)
{
@@ -1372,19 +1386,18 @@ public class UserService : UserManager, IUserService, IDisposable
private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user)
{
var twoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication);
- var organizationsManagingUser = await GetOrganizationsManagingUserAsync(user.Id);
var removeOrgUserTasks = twoFactorPolicies.Select(async p =>
{
var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);
- if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && organizationsManagingUser.Any(o => o.Id == p.OrganizationId))
+ if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
{
await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
new RevokeOrganizationUsersRequest(
p.OrganizationId,
- [new OrganizationUserUserDetails { UserId = user.Id, OrganizationId = p.OrganizationId }],
+ [new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }],
new SystemUser(EventSystemUser.TwoFactorDisabled)));
- await _mailService.SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(organization.DisplayName(), user.Email);
+ await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email);
}
else
{
diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs
index 4ce188c86b..d6b330294d 100644
--- a/src/Core/Services/NoopImplementations/NoopMailService.cs
+++ b/src/Core/Services/NoopImplementations/NoopMailService.cs
@@ -26,6 +26,7 @@ public class NoopMailService : IMailService
}
public Task SendTrialInitiationSignupEmailAsync(
+ bool isExistingUser,
string email,
string token,
ProductTierType productTier,
@@ -80,7 +81,7 @@ public class NoopMailService : IMailService
return Task.FromResult(0);
}
- public Task SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(string organizationName, string email) =>
+ public Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email) =>
Task.CompletedTask;
public Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email) =>
@@ -316,5 +317,10 @@ public class NoopMailService : IMailService
return Task.FromResult(0);
}
public Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList) => Task.CompletedTask;
+
+ public Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable adminEmails, Guid organizationId, string email, string userName)
+ {
+ return Task.FromResult(0);
+ }
}
diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs
index 420151a34f..a1c7a4fac6 100644
--- a/src/Core/Settings/GlobalSettings.cs
+++ b/src/Core/Settings/GlobalSettings.cs
@@ -53,6 +53,7 @@ public class GlobalSettings : IGlobalSettings
public virtual SqlSettings PostgreSql { get; set; } = new SqlSettings();
public virtual SqlSettings MySql { get; set; } = new SqlSettings();
public virtual SqlSettings Sqlite { get; set; } = new SqlSettings() { ConnectionString = "Data Source=:memory:" };
+ public virtual EventLoggingSettings EventLogging { get; set; } = new EventLoggingSettings();
public virtual MailSettings Mail { get; set; } = new MailSettings();
public virtual IConnectionStringSettings Storage { get; set; } = new ConnectionStringSettings();
public virtual ConnectionStringSettings Events { get; set; } = new ConnectionStringSettings();
@@ -69,6 +70,7 @@ public class GlobalSettings : IGlobalSettings
public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings();
public virtual DuoSettings Duo { get; set; } = new DuoSettings();
public virtual BraintreeSettings Braintree { get; set; } = new BraintreeSettings();
+ public virtual ImportCiphersLimitationSettings ImportCiphersLimitation { get; set; } = new ImportCiphersLimitationSettings();
public virtual BitPaySettings BitPay { get; set; } = new BitPaySettings();
public virtual AmazonSettings Amazon { get; set; } = new AmazonSettings();
public virtual ServiceBusSettings ServiceBus { get; set; } = new ServiceBusSettings();
@@ -82,6 +84,7 @@ public class GlobalSettings : IGlobalSettings
public virtual ILaunchDarklySettings LaunchDarkly { get; set; } = new LaunchDarklySettings();
public virtual string DevelopmentDirectory { get; set; }
public virtual bool EnableEmailVerification { get; set; }
+ public virtual string KdfDefaultHashKey { get; set; }
public virtual string PricingUri { get; set; }
public string BuildExternalUri(string explicitValue, string name)
@@ -255,6 +258,66 @@ public class GlobalSettings : IGlobalSettings
}
}
+ public class EventLoggingSettings
+ {
+ public AzureServiceBusSettings AzureServiceBus { get; set; } = new AzureServiceBusSettings();
+ public virtual string WebhookUrl { get; set; }
+ public RabbitMqSettings RabbitMq { get; set; } = new RabbitMqSettings();
+
+ public class AzureServiceBusSettings
+ {
+ private string _connectionString;
+ private string _topicName;
+
+ public virtual string EventRepositorySubscriptionName { get; set; } = "events-write-subscription";
+ public virtual string WebhookSubscriptionName { get; set; } = "events-webhook-subscription";
+
+ public string ConnectionString
+ {
+ get => _connectionString;
+ set => _connectionString = value.Trim('"');
+ }
+
+ public string TopicName
+ {
+ get => _topicName;
+ set => _topicName = value.Trim('"');
+ }
+ }
+
+ public class RabbitMqSettings
+ {
+ private string _hostName;
+ private string _username;
+ private string _password;
+ private string _exchangeName;
+
+ public virtual string EventRepositoryQueueName { get; set; } = "events-write-queue";
+ public virtual string WebhookQueueName { get; set; } = "events-webhook-queue";
+
+ public string HostName
+ {
+ get => _hostName;
+ set => _hostName = value.Trim('"');
+ }
+ public string Username
+ {
+ get => _username;
+ set => _username = value.Trim('"');
+ }
+ public string Password
+ {
+ get => _password;
+ set => _password = value.Trim('"');
+ }
+ public string ExchangeName
+ {
+ get => _exchangeName;
+ set => _exchangeName = value.Trim('"');
+ }
+ }
+ }
+
public class ConnectionStringSettings : IConnectionStringSettings
{
private string _connectionString;
@@ -481,6 +544,13 @@ public class GlobalSettings : IGlobalSettings
public string PrivateKey { get; set; }
}
+ public class ImportCiphersLimitationSettings
+ {
+ public int CiphersLimit { get; set; }
+ public int CollectionRelationshipsLimit { get; set; }
+ public int CollectionsLimit { get; set; }
+ }
+
public class BitPaySettings
{
public bool Production { get; set; }
diff --git a/src/Core/Settings/IGlobalSettings.cs b/src/Core/Settings/IGlobalSettings.cs
index afe35ed34b..b89df8abf5 100644
--- a/src/Core/Settings/IGlobalSettings.cs
+++ b/src/Core/Settings/IGlobalSettings.cs
@@ -27,4 +27,5 @@ public interface IGlobalSettings
string DatabaseProvider { get; set; }
GlobalSettings.SqlSettings SqlServer { get; set; }
string DevelopmentDirectory { get; set; }
+ GlobalSettings.EventLoggingSettings EventLogging { get; set; }
}
diff --git a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs
new file mode 100644
index 0000000000..646121db52
--- /dev/null
+++ b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs
@@ -0,0 +1,199 @@
+using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Services;
+using Bit.Core.Context;
+using Bit.Core.Entities;
+using Bit.Core.Exceptions;
+using Bit.Core.Platform.Push;
+using Bit.Core.Repositories;
+using Bit.Core.Tools.Enums;
+using Bit.Core.Tools.ImportFeatures.Interfaces;
+using Bit.Core.Tools.Models.Business;
+using Bit.Core.Tools.Services;
+using Bit.Core.Vault.Entities;
+using Bit.Core.Vault.Models.Data;
+using Bit.Core.Vault.Repositories;
+
+namespace Bit.Core.Tools.ImportFeatures;
+
+public class ImportCiphersCommand : IImportCiphersCommand
+{
+ private readonly ICipherRepository _cipherRepository;
+ private readonly IFolderRepository _folderRepository;
+ private readonly IPushNotificationService _pushService;
+ private readonly IPolicyService _policyService;
+ private readonly IOrganizationRepository _organizationRepository;
+ private readonly IOrganizationUserRepository _organizationUserRepository;
+ private readonly ICollectionRepository _collectionRepository;
+ private readonly IReferenceEventService _referenceEventService;
+ private readonly ICurrentContext _currentContext;
+
+
+ public ImportCiphersCommand(
+ ICipherRepository cipherRepository,
+ IFolderRepository folderRepository,
+ ICollectionRepository collectionRepository,
+ IOrganizationRepository organizationRepository,
+ IOrganizationUserRepository organizationUserRepository,
+ IPushNotificationService pushService,
+ IPolicyService policyService,
+ IReferenceEventService referenceEventService,
+ ICurrentContext currentContext)
+ {
+ _cipherRepository = cipherRepository;
+ _folderRepository = folderRepository;
+ _organizationRepository = organizationRepository;
+ _organizationUserRepository = organizationUserRepository;
+ _collectionRepository = collectionRepository;
+ _pushService = pushService;
+ _policyService = policyService;
+ _referenceEventService = referenceEventService;
+ _currentContext = currentContext;
+ }
+
+
+ public async Task ImportIntoIndividualVaultAsync(
+ List folders,
+ List ciphers,
+ IEnumerable> folderRelationships)
+ {
+ var userId = folders.FirstOrDefault()?.UserId ?? ciphers.FirstOrDefault()?.UserId;
+
+ // Make sure the user can save new ciphers to their personal vault
+ var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId.Value, PolicyType.PersonalOwnership);
+ if (anyPersonalOwnershipPolicies)
+ {
+ throw new BadRequestException("You cannot import items into your personal vault because you are " +
+ "a member of an organization which forbids it.");
+ }
+
+ foreach (var cipher in ciphers)
+ {
+ cipher.SetNewId();
+
+ if (cipher.UserId.HasValue && cipher.Favorite)
+ {
+ cipher.Favorites = $"{{\"{cipher.UserId.ToString().ToUpperInvariant()}\":\"true\"}}";
+ }
+ }
+
+ var userfoldersIds = (await _folderRepository.GetManyByUserIdAsync(userId ?? Guid.Empty)).Select(f => f.Id).ToList();
+
+ //Assign id to the ones that don't exist in DB
+ //Need to keep the list order to create the relationships
+ List newFolders = new List();
+ foreach (var folder in folders)
+ {
+ if (!userfoldersIds.Contains(folder.Id))
+ {
+ folder.SetNewId();
+ newFolders.Add(folder);
+ }
+ }
+
+ // Create the folder associations based on the newly created folder ids
+ foreach (var relationship in folderRelationships)
+ {
+ var cipher = ciphers.ElementAtOrDefault(relationship.Key);
+ var folder = folders.ElementAtOrDefault(relationship.Value);
+
+ if (cipher == null || folder == null)
+ {
+ continue;
+ }
+
+ cipher.Folders = $"{{\"{cipher.UserId.ToString().ToUpperInvariant()}\":" +
+ $"\"{folder.Id.ToString().ToUpperInvariant()}\"}}";
+ }
+
+ // Create it all
+ await _cipherRepository.CreateAsync(ciphers, newFolders);
+
+ // push
+ if (userId.HasValue)
+ {
+ await _pushService.PushSyncVaultAsync(userId.Value);
+ }
+ }
+
+ public async Task ImportIntoOrganizationalVaultAsync(
+ List collections,
+ List ciphers,
+ IEnumerable> collectionRelationships,
+ Guid importingUserId)
+ {
+ var org = collections.Count > 0 ?
+ await _organizationRepository.GetByIdAsync(collections[0].OrganizationId) :
+ await _organizationRepository.GetByIdAsync(ciphers.FirstOrDefault(c => c.OrganizationId.HasValue).OrganizationId.Value);
+ var importingOrgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, importingUserId);
+
+ if (collections.Count > 0 && org != null && org.MaxCollections.HasValue)
+ {
+ var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(org.Id);
+ if (org.MaxCollections.Value < (collectionCount + collections.Count))
+ {
+ throw new BadRequestException("This organization can only have a maximum of " +
+ $"{org.MaxCollections.Value} collections.");
+ }
+ }
+
+ // Init. ids for ciphers
+ foreach (var cipher in ciphers)
+ {
+ cipher.SetNewId();
+ }
+
+ var organizationCollectionsIds = (await _collectionRepository.GetManyByOrganizationIdAsync(org.Id)).Select(c => c.Id).ToList();
+
+ //Assign id to the ones that don't exist in DB
+ //Need to keep the list order to create the relationships
+ var newCollections = new List();
+ var newCollectionUsers = new List();
+
+ foreach (var collection in collections)
+ {
+ if (!organizationCollectionsIds.Contains(collection.Id))
+ {
+ collection.SetNewId();
+ newCollections.Add(collection);
+ newCollectionUsers.Add(new CollectionUser
+ {
+ CollectionId = collection.Id,
+ OrganizationUserId = importingOrgUser.Id,
+ Manage = true
+ });
+ }
+ }
+
+ // Create associations based on the newly assigned ids
+ var collectionCiphers = new List();
+ foreach (var relationship in collectionRelationships)
+ {
+ var cipher = ciphers.ElementAtOrDefault(relationship.Key);
+ var collection = collections.ElementAtOrDefault(relationship.Value);
+
+ if (cipher == null || collection == null)
+ {
+ continue;
+ }
+
+ collectionCiphers.Add(new CollectionCipher
+ {
+ CipherId = cipher.Id,
+ CollectionId = collection.Id
+ });
+ }
+
+ // Create it all
+ await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers, newCollectionUsers);
+
+ // push
+ await _pushService.PushSyncVaultAsync(importingUserId);
+
+
+ if (org != null)
+ {
+ await _referenceEventService.RaiseEventAsync(
+ new ReferenceEvent(ReferenceEventType.VaultImported, org, _currentContext));
+ }
+ }
+}
diff --git a/src/Core/Tools/ImportFeatures/ImportServiceCollectionExtension.cs b/src/Core/Tools/ImportFeatures/ImportServiceCollectionExtension.cs
new file mode 100644
index 0000000000..38c88d7994
--- /dev/null
+++ b/src/Core/Tools/ImportFeatures/ImportServiceCollectionExtension.cs
@@ -0,0 +1,12 @@
+using Bit.Core.Tools.ImportFeatures.Interfaces;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Bit.Core.Tools.ImportFeatures;
+
+public static class ImportServiceCollectionExtension
+{
+ public static void AddImportServices(this IServiceCollection services)
+ {
+ services.AddScoped();
+ }
+}
diff --git a/src/Core/Tools/ImportFeatures/Interfaces/IImportCiphersCommand.cs b/src/Core/Tools/ImportFeatures/Interfaces/IImportCiphersCommand.cs
new file mode 100644
index 0000000000..378024d3a0
--- /dev/null
+++ b/src/Core/Tools/ImportFeatures/Interfaces/IImportCiphersCommand.cs
@@ -0,0 +1,14 @@
+using Bit.Core.Entities;
+using Bit.Core.Vault.Entities;
+using Bit.Core.Vault.Models.Data;
+
+namespace Bit.Core.Tools.ImportFeatures.Interfaces;
+
+public interface IImportCiphersCommand
+{
+ Task ImportIntoIndividualVaultAsync(List folders, List ciphers,
+ IEnumerable> folderRelationships);
+
+ Task ImportIntoOrganizationalVaultAsync(List collections, List ciphers,
+ IEnumerable> collectionRelationships, Guid importingUserId);
+}
diff --git a/src/Core/Vault/Authorization/SecurityTaskOperations.cs b/src/Core/Vault/Authorization/SecurityTaskOperations.cs
deleted file mode 100644
index 77b504723f..0000000000
--- a/src/Core/Vault/Authorization/SecurityTaskOperations.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Microsoft.AspNetCore.Authorization.Infrastructure;
-
-namespace Bit.Core.Vault.Authorization;
-
-public class SecurityTaskOperationRequirement : OperationAuthorizationRequirement
-{
- public SecurityTaskOperationRequirement(string name)
- {
- Name = name;
- }
-}
-
-public static class SecurityTaskOperations
-{
- public static readonly SecurityTaskOperationRequirement Update = new(nameof(Update));
-}
diff --git a/src/Core/Vault/Commands/CreateManyTasksCommand.cs b/src/Core/Vault/Commands/CreateManyTasksCommand.cs
new file mode 100644
index 0000000000..1b21f202eb
--- /dev/null
+++ b/src/Core/Vault/Commands/CreateManyTasksCommand.cs
@@ -0,0 +1,65 @@
+using Bit.Core.Context;
+using Bit.Core.Exceptions;
+using Bit.Core.Utilities;
+using Bit.Core.Vault.Authorization.SecurityTasks;
+using Bit.Core.Vault.Commands.Interfaces;
+using Bit.Core.Vault.Entities;
+using Bit.Core.Vault.Enums;
+using Bit.Core.Vault.Models.Api;
+using Bit.Core.Vault.Repositories;
+using Microsoft.AspNetCore.Authorization;
+
+namespace Bit.Core.Vault.Commands;
+
+public class CreateManyTasksCommand : ICreateManyTasksCommand
+{
+ private readonly IAuthorizationService _authorizationService;
+ private readonly ICurrentContext _currentContext;
+ private readonly ISecurityTaskRepository _securityTaskRepository;
+
+ public CreateManyTasksCommand(
+ ISecurityTaskRepository securityTaskRepository,
+ IAuthorizationService authorizationService,
+ ICurrentContext currentContext)
+ {
+ _securityTaskRepository = securityTaskRepository;
+ _authorizationService = authorizationService;
+ _currentContext = currentContext;
+ }
+
+ ///
+ public async Task> CreateAsync(Guid organizationId,
+ IEnumerable tasks)
+ {
+ if (!_currentContext.UserId.HasValue)
+ {
+ throw new NotFoundException();
+ }
+
+ var tasksList = tasks?.ToList();
+
+ if (tasksList is null || tasksList.Count == 0)
+ {
+ throw new BadRequestException("No tasks provided.");
+ }
+
+ var securityTasks = tasksList.Select(t => new SecurityTask
+ {
+ OrganizationId = organizationId,
+ CipherId = t.CipherId,
+ Type = t.Type,
+ Status = SecurityTaskStatus.Pending
+ }).ToList();
+
+ // Verify authorization for each task
+ foreach (var task in securityTasks)
+ {
+ await _authorizationService.AuthorizeOrThrowAsync(
+ _currentContext.HttpContext.User,
+ task,
+ SecurityTaskOperations.Create);
+ }
+
+ return await _securityTaskRepository.CreateManyAsync(securityTasks);
+ }
+}
diff --git a/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs b/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs
new file mode 100644
index 0000000000..3aa0f85070
--- /dev/null
+++ b/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs
@@ -0,0 +1,17 @@
+using Bit.Core.Vault.Entities;
+using Bit.Core.Vault.Models.Api;
+
+namespace Bit.Core.Vault.Commands.Interfaces;
+
+public interface ICreateManyTasksCommand
+{
+ ///
+ /// Creates multiple security tasks for an organization.
+ /// Each task must be authorized and the user must have the Create permission
+ /// and associated ciphers must belong to the organization.
+ ///
+ /// The
+ ///
+ /// Collection of created security tasks
+ Task> CreateAsync(Guid organizationId, IEnumerable tasks);
+}
diff --git a/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs b/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs
index b46fb0cecb..77b8a8625c 100644
--- a/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs
+++ b/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs
@@ -1,7 +1,7 @@
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
-using Bit.Core.Vault.Authorization;
+using Bit.Core.Vault.Authorization.SecurityTasks;
using Bit.Core.Vault.Commands.Interfaces;
using Bit.Core.Vault.Enums;
using Bit.Core.Vault.Repositories;
diff --git a/src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs b/src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs
new file mode 100644
index 0000000000..f865871380
--- /dev/null
+++ b/src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs
@@ -0,0 +1,9 @@
+using Bit.Core.Vault.Enums;
+
+namespace Bit.Core.Vault.Models.Api;
+
+public class SecurityTaskCreateRequest
+{
+ public SecurityTaskType Type { get; set; }
+ public Guid? CipherId { get; set; }
+}
diff --git a/src/Core/Vault/Models/Data/DeleteAttachmentReponseData.cs b/src/Core/Vault/Models/Data/DeleteAttachmentReponseData.cs
new file mode 100644
index 0000000000..0a5e755572
--- /dev/null
+++ b/src/Core/Vault/Models/Data/DeleteAttachmentReponseData.cs
@@ -0,0 +1,13 @@
+using Bit.Core.Vault.Entities;
+
+namespace Bit.Core.Vault.Models.Data;
+
+public class DeleteAttachmentResponseData
+{
+ public Cipher Cipher { get; set; }
+
+ public DeleteAttachmentResponseData(Cipher cipher)
+ {
+ Cipher = cipher;
+ }
+}
diff --git a/src/Core/Vault/Repositories/ISecurityTaskRepository.cs b/src/Core/Vault/Repositories/ISecurityTaskRepository.cs
index c236172533..cc8303345d 100644
--- a/src/Core/Vault/Repositories/ISecurityTaskRepository.cs
+++ b/src/Core/Vault/Repositories/ISecurityTaskRepository.cs
@@ -21,4 +21,11 @@ public interface ISecurityTaskRepository : IRepository
/// Optional filter for task status. If not provided, returns tasks of all statuses
///
Task> GetManyByOrganizationIdStatusAsync(Guid organizationId, SecurityTaskStatus? status = null);
+
+ ///
+ /// Creates bulk security tasks for an organization.
+ ///
+ /// Collection of tasks to create
+ /// Collection of created security tasks
+ Task> CreateManyAsync(IEnumerable tasks);
}
diff --git a/src/Core/Vault/Services/ICipherService.cs b/src/Core/Vault/Services/ICipherService.cs
index 83cd729e13..17f55cb47d 100644
--- a/src/Core/Vault/Services/ICipherService.cs
+++ b/src/Core/Vault/Services/ICipherService.cs
@@ -1,5 +1,4 @@
-using Bit.Core.Entities;
-using Bit.Core.Vault.Entities;
+using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Models.Data;
namespace Bit.Core.Vault.Services;
@@ -18,7 +17,7 @@ public interface ICipherService
string attachmentId, Guid organizationShareId);
Task DeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false);
Task DeleteManyAsync(IEnumerable cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false);
- Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false);
+ Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false);
Task PurgeAsync(Guid organizationId);
Task MoveManyAsync(IEnumerable cipherIds, Guid? destinationFolderId, Guid movingUserId);
Task SaveFolderAsync(Folder folder);
@@ -28,10 +27,6 @@ public interface ICipherService
Task ShareManyAsync(IEnumerable<(Cipher cipher, DateTime? lastKnownRevisionDate)> ciphers, Guid organizationId,
IEnumerable collectionIds, Guid sharingUserId);
Task SaveCollectionsAsync(Cipher cipher, IEnumerable collectionIds, Guid savingUserId, bool orgAdmin);
- Task ImportCiphersAsync(List folders, List ciphers,
- IEnumerable> folderRelationships);
- Task ImportCiphersAsync(List collections, List ciphers,
- IEnumerable> collectionRelationships, Guid importingUserId);
Task SoftDeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false);
Task SoftDeleteManyAsync(IEnumerable cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false);
Task RestoreAsync(Cipher cipher, Guid restoringUserId, bool orgAdmin = false);
@@ -39,5 +34,4 @@ public interface ICipherService
Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId);
Task GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId);
Task ValidateCipherAttachmentFile(Cipher cipher, CipherAttachment.MetaData attachmentData);
- Task<(IEnumerable, Dictionary>)> GetOrganizationCiphers(Guid userId, Guid organizationId);
}
diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs
index d6806bd115..90c03df90b 100644
--- a/src/Core/Vault/Services/Implementations/CipherService.cs
+++ b/src/Core/Vault/Services/Implementations/CipherService.cs
@@ -2,7 +2,6 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Context;
-using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
@@ -211,6 +210,11 @@ public class CipherService : ICipherService
AttachmentData = JsonSerializer.Serialize(data)
});
cipher.AddAttachment(attachmentId, data);
+
+ // Update the revision date when an attachment is added
+ cipher.RevisionDate = DateTime.UtcNow;
+ await _cipherRepository.ReplaceAsync((CipherDetails)cipher);
+
await _pushService.PushSyncCipherUpdateAsync(cipher, null);
return (attachmentId, uploadUrl);
@@ -260,6 +264,10 @@ public class CipherService : ICipherService
throw;
}
+ // Update the revision date when an attachment is added
+ cipher.RevisionDate = DateTime.UtcNow;
+ await _cipherRepository.ReplaceAsync((CipherDetails)cipher);
+
// push
await _pushService.PushSyncCipherUpdateAsync(cipher, null);
}
@@ -442,7 +450,7 @@ public class CipherService : ICipherService
await _pushService.PushSyncCiphersAsync(deletingUserId);
}
- public async Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId,
+ public async Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId,
bool orgAdmin = false)
{
if (!orgAdmin && !(await UserCanEditAsync(cipher, deletingUserId)))
@@ -455,7 +463,7 @@ public class CipherService : ICipherService
throw new NotFoundException();
}
- await DeleteAttachmentAsync(cipher, cipher.GetAttachments()[attachmentId]);
+ return await DeleteAttachmentAsync(cipher, cipher.GetAttachments()[attachmentId]);
}
public async Task PurgeAsync(Guid organizationId)
@@ -679,152 +687,6 @@ public class CipherService : ICipherService
await _pushService.PushSyncCipherUpdateAsync(cipher, collectionIds);
}
- public async Task ImportCiphersAsync(
- List folders,
- List ciphers,
- IEnumerable> folderRelationships)
- {
- var userId = folders.FirstOrDefault()?.UserId ?? ciphers.FirstOrDefault()?.UserId;
-
- // Make sure the user can save new ciphers to their personal vault
- var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId.Value, PolicyType.PersonalOwnership);
- if (anyPersonalOwnershipPolicies)
- {
- throw new BadRequestException("You cannot import items into your personal vault because you are " +
- "a member of an organization which forbids it.");
- }
-
- foreach (var cipher in ciphers)
- {
- cipher.SetNewId();
-
- if (cipher.UserId.HasValue && cipher.Favorite)
- {
- cipher.Favorites = $"{{\"{cipher.UserId.ToString().ToUpperInvariant()}\":\"true\"}}";
- }
- }
-
- var userfoldersIds = (await _folderRepository.GetManyByUserIdAsync(userId ?? Guid.Empty)).Select(f => f.Id).ToList();
-
- //Assign id to the ones that don't exist in DB
- //Need to keep the list order to create the relationships
- List newFolders = new List();
- foreach (var folder in folders)
- {
- if (!userfoldersIds.Contains(folder.Id))
- {
- folder.SetNewId();
- newFolders.Add(folder);
- }
- }
-
- // Create the folder associations based on the newly created folder ids
- foreach (var relationship in folderRelationships)
- {
- var cipher = ciphers.ElementAtOrDefault(relationship.Key);
- var folder = folders.ElementAtOrDefault(relationship.Value);
-
- if (cipher == null || folder == null)
- {
- continue;
- }
-
- cipher.Folders = $"{{\"{cipher.UserId.ToString().ToUpperInvariant()}\":" +
- $"\"{folder.Id.ToString().ToUpperInvariant()}\"}}";
- }
-
- // Create it all
- await _cipherRepository.CreateAsync(ciphers, newFolders);
-
- // push
- if (userId.HasValue)
- {
- await _pushService.PushSyncVaultAsync(userId.Value);
- }
- }
-
- public async Task ImportCiphersAsync(
- List collections,
- List ciphers,
- IEnumerable> collectionRelationships,
- Guid importingUserId)
- {
- var org = collections.Count > 0 ?
- await _organizationRepository.GetByIdAsync(collections[0].OrganizationId) :
- await _organizationRepository.GetByIdAsync(ciphers.FirstOrDefault(c => c.OrganizationId.HasValue).OrganizationId.Value);
- var importingOrgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, importingUserId);
-
- if (collections.Count > 0 && org != null && org.MaxCollections.HasValue)
- {
- var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(org.Id);
- if (org.MaxCollections.Value < (collectionCount + collections.Count))
- {
- throw new BadRequestException("This organization can only have a maximum of " +
- $"{org.MaxCollections.Value} collections.");
- }
- }
-
- // Init. ids for ciphers
- foreach (var cipher in ciphers)
- {
- cipher.SetNewId();
- }
-
- var organizationCollectionsIds = (await _collectionRepository.GetManyByOrganizationIdAsync(org.Id)).Select(c => c.Id).ToList();
-
- //Assign id to the ones that don't exist in DB
- //Need to keep the list order to create the relationships
- var newCollections = new List();
- var newCollectionUsers = new List();
-
- foreach (var collection in collections)
- {
- if (!organizationCollectionsIds.Contains(collection.Id))
- {
- collection.SetNewId();
- newCollections.Add(collection);
- newCollectionUsers.Add(new CollectionUser
- {
- CollectionId = collection.Id,
- OrganizationUserId = importingOrgUser.Id,
- Manage = true
- });
- }
- }
-
- // Create associations based on the newly assigned ids
- var collectionCiphers = new List();
- foreach (var relationship in collectionRelationships)
- {
- var cipher = ciphers.ElementAtOrDefault(relationship.Key);
- var collection = collections.ElementAtOrDefault(relationship.Value);
-
- if (cipher == null || collection == null)
- {
- continue;
- }
-
- collectionCiphers.Add(new CollectionCipher
- {
- CipherId = cipher.Id,
- CollectionId = collection.Id
- });
- }
-
- // Create it all
- await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers, newCollectionUsers);
-
- // push
- await _pushService.PushSyncVaultAsync(importingUserId);
-
-
- if (org != null)
- {
- await _referenceEventService.RaiseEventAsync(
- new ReferenceEvent(ReferenceEventType.VaultImported, org, _currentContext));
- }
- }
-
public async Task SoftDeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false)
{
if (!orgAdmin && !(await UserCanEditAsync(cipher, deletingUserId)))
@@ -956,35 +818,6 @@ public class CipherService : ICipherService
return restoringCiphers;
}
- public async Task<(IEnumerable, Dictionary