mirror of
https://github.com/bitwarden/server.git
synced 2025-06-23 20:28:55 -05:00
Merge branch 'main' into dirt/pm-20576/report-store-query-commands
This commit is contained in:
commit
48b967bc79
3
.github/workflows/enforce-labels.yml
vendored
3
.github/workflows/enforce-labels.yml
vendored
@ -4,6 +4,9 @@ on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
types: [labeled, unlabeled, opened, reopened, synchronize]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
enforce-label:
|
||||
if: ${{ contains(github.event.*.labels.*.name, 'hold') || contains(github.event.*.labels.*.name, 'needs-qa') || contains(github.event.*.labels.*.name, 'DB-migrations-changed') || contains(github.event.*.labels.*.name, 'ephemeral-environment') }}
|
||||
|
3
.github/workflows/protect-files.yml
vendored
3
.github/workflows/protect-files.yml
vendored
@ -16,6 +16,9 @@ jobs:
|
||||
changed-files:
|
||||
name: Check for file changes
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
outputs:
|
||||
changes: ${{steps.check-changes.outputs.changes_detected}}
|
||||
|
||||
|
125
.github/workflows/repository-management.yml
vendored
125
.github/workflows/repository-management.yml
vendored
@ -22,6 +22,8 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Setup
|
||||
@ -44,49 +46,11 @@ jobs:
|
||||
|
||||
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
cut_branch:
|
||||
name: Cut branch
|
||||
if: ${{ needs.setup.outputs.branch != 'none' }}
|
||||
needs: setup
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||
|
||||
- name: Check out target ref
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.target_ref }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Check if ${{ needs.setup.outputs.branch }} branch exists
|
||||
env:
|
||||
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
|
||||
run: |
|
||||
if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then
|
||||
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Cut branch
|
||||
env:
|
||||
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
|
||||
run: |
|
||||
git switch --quiet --create $BRANCH_NAME
|
||||
git push --quiet --set-upstream origin $BRANCH_NAME
|
||||
|
||||
|
||||
bump_version:
|
||||
name: Bump Version
|
||||
if: ${{ always() }}
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- cut_branch
|
||||
- setup
|
||||
outputs:
|
||||
version: ${{ steps.set-final-version-output.outputs.version }}
|
||||
@ -187,14 +151,13 @@ jobs:
|
||||
- name: Push changes
|
||||
run: git push
|
||||
|
||||
|
||||
cherry_pick:
|
||||
name: Cherry-Pick Commit(s)
|
||||
cut_branch:
|
||||
name: Cut branch
|
||||
if: ${{ needs.setup.outputs.branch != 'none' }}
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- bump_version
|
||||
- setup
|
||||
- bump_version
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||
@ -203,78 +166,30 @@ jobs:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||
|
||||
- name: Check out main branch
|
||||
- name: Check out target ref
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
ref: ${{ inputs.target_ref }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config --local user.email "actions@github.com"
|
||||
git config --local user.name "Github Actions"
|
||||
|
||||
- name: Install xmllint
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxml2-utils
|
||||
|
||||
- name: Perform cherry-pick(s)
|
||||
- name: Check if ${{ needs.setup.outputs.branch }} branch exists
|
||||
env:
|
||||
CUT_BRANCH: ${{ needs.setup.outputs.branch }}
|
||||
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
|
||||
run: |
|
||||
# Function for cherry-picking
|
||||
cherry_pick () {
|
||||
local source_branch=$1
|
||||
local destination_branch=$2
|
||||
|
||||
# Get project commit/version from source branch
|
||||
git switch $source_branch
|
||||
SOURCE_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 Directory.Build.props)
|
||||
SOURCE_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
|
||||
|
||||
# Get project commit/version from destination branch
|
||||
git switch $destination_branch
|
||||
DESTINATION_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
|
||||
|
||||
if [[ "$DESTINATION_VERSION" != "$SOURCE_VERSION" ]]; then
|
||||
git cherry-pick --strategy-option=theirs -x $SOURCE_COMMIT
|
||||
git push -u origin $destination_branch
|
||||
fi
|
||||
}
|
||||
|
||||
# If we are cutting 'hotfix-rc':
|
||||
if [[ "$CUT_BRANCH" == "hotfix-rc" ]]; then
|
||||
|
||||
# If the 'rc' branch exists:
|
||||
if [[ $(git ls-remote --heads origin rc) ]]; then
|
||||
|
||||
# Chery-pick from 'rc' into 'hotfix-rc'
|
||||
cherry_pick rc hotfix-rc
|
||||
|
||||
# Cherry-pick from 'main' into 'rc'
|
||||
cherry_pick main rc
|
||||
|
||||
# If the 'rc' branch does not exist:
|
||||
else
|
||||
|
||||
# Cherry-pick from 'main' into 'hotfix-rc'
|
||||
cherry_pick main hotfix-rc
|
||||
|
||||
fi
|
||||
|
||||
# If we are cutting 'rc':
|
||||
elif [[ "$CUT_BRANCH" == "rc" ]]; then
|
||||
|
||||
# Cherry-pick from 'main' into 'rc'
|
||||
cherry_pick main rc
|
||||
|
||||
if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then
|
||||
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Cut branch
|
||||
env:
|
||||
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
|
||||
run: |
|
||||
git switch --quiet --create $BRANCH_NAME
|
||||
git push --quiet --set-upstream origin $BRANCH_NAME
|
||||
|
||||
move_future_db_scripts:
|
||||
name: Move finalization database scripts
|
||||
needs: cherry_pick
|
||||
needs: cut_branch
|
||||
uses: ./.github/workflows/_move_finalization_db_scripts.yml
|
||||
secrets: inherit
|
||||
|
5
.github/workflows/stale-bot.yml
vendored
5
.github/workflows/stale-bot.yml
vendored
@ -8,6 +8,11 @@ jobs:
|
||||
stale:
|
||||
name: Check for stale issues and PRs
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check
|
||||
uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
|
||||
|
7
.github/workflows/test-database.yml
vendored
7
.github/workflows/test-database.yml
vendored
@ -31,10 +31,17 @@ on:
|
||||
- "test/Infrastructure.IntegrationTest/**" # Any changes to the tests
|
||||
- "src/**/Entities/**/*.cs" # Database entity definitions
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
@ -1,8 +1,11 @@
|
||||
using Bit.Core.Context;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.NotificationHub;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Platform.Push.Internal;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -20,14 +23,14 @@ namespace Bit.Api.Platform.Push;
|
||||
public class PushController : Controller
|
||||
{
|
||||
private readonly IPushRegistrationService _pushRegistrationService;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly IPushRelayer _pushRelayer;
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
|
||||
public PushController(
|
||||
IPushRegistrationService pushRegistrationService,
|
||||
IPushNotificationService pushNotificationService,
|
||||
IPushRelayer pushRelayer,
|
||||
IWebHostEnvironment environment,
|
||||
ICurrentContext currentContext,
|
||||
IGlobalSettings globalSettings)
|
||||
@ -35,7 +38,7 @@ public class PushController : Controller
|
||||
_currentContext = currentContext;
|
||||
_environment = environment;
|
||||
_pushRegistrationService = pushRegistrationService;
|
||||
_pushNotificationService = pushNotificationService;
|
||||
_pushRelayer = pushRelayer;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
@ -74,31 +77,50 @@ public class PushController : Controller
|
||||
}
|
||||
|
||||
[HttpPost("send")]
|
||||
public async Task SendAsync([FromBody] PushSendRequestModel model)
|
||||
public async Task SendAsync([FromBody] PushSendRequestModel<JsonElement> model)
|
||||
{
|
||||
CheckUsage();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(model.InstallationId))
|
||||
NotificationTarget target;
|
||||
Guid targetId;
|
||||
|
||||
if (model.InstallationId.HasValue)
|
||||
{
|
||||
if (_currentContext.InstallationId!.Value.ToString() != model.InstallationId!)
|
||||
if (_currentContext.InstallationId!.Value != model.InstallationId.Value)
|
||||
{
|
||||
throw new BadRequestException("InstallationId does not match current context.");
|
||||
}
|
||||
|
||||
await _pushNotificationService.SendPayloadToInstallationAsync(
|
||||
_currentContext.InstallationId.Value.ToString(), model.Type, model.Payload, Prefix(model.Identifier),
|
||||
Prefix(model.DeviceId), model.ClientType);
|
||||
target = NotificationTarget.Installation;
|
||||
targetId = _currentContext.InstallationId.Value;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(model.UserId))
|
||||
else if (model.UserId.HasValue)
|
||||
{
|
||||
await _pushNotificationService.SendPayloadToUserAsync(Prefix(model.UserId),
|
||||
model.Type, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId), model.ClientType);
|
||||
target = NotificationTarget.User;
|
||||
targetId = model.UserId.Value;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(model.OrganizationId))
|
||||
else if (model.OrganizationId.HasValue)
|
||||
{
|
||||
await _pushNotificationService.SendPayloadToOrganizationAsync(Prefix(model.OrganizationId),
|
||||
model.Type, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId), model.ClientType);
|
||||
target = NotificationTarget.Organization;
|
||||
targetId = model.OrganizationId.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new UnreachableException("Model validation should have prevented getting here.");
|
||||
}
|
||||
|
||||
var notification = new RelayedNotification
|
||||
{
|
||||
Type = model.Type,
|
||||
Target = target,
|
||||
TargetId = targetId,
|
||||
Payload = model.Payload,
|
||||
Identifier = model.Identifier,
|
||||
DeviceId = model.DeviceId,
|
||||
ClientType = model.ClientType,
|
||||
};
|
||||
|
||||
await _pushRelayer.RelayAsync(_currentContext.InstallationId.Value, notification);
|
||||
}
|
||||
|
||||
private string Prefix(string value)
|
||||
|
@ -20,6 +20,7 @@ public class RabbitMqIntegrationListenerService : BackgroundService
|
||||
private readonly Lazy<Task<IChannel>> _lazyChannel;
|
||||
private readonly IRabbitMqService _rabbitMqService;
|
||||
private readonly ILogger<RabbitMqIntegrationListenerService> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public RabbitMqIntegrationListenerService(IIntegrationHandler handler,
|
||||
string routingKey,
|
||||
@ -27,7 +28,8 @@ public class RabbitMqIntegrationListenerService : BackgroundService
|
||||
string retryQueueName,
|
||||
int maxRetries,
|
||||
IRabbitMqService rabbitMqService,
|
||||
ILogger<RabbitMqIntegrationListenerService> logger)
|
||||
ILogger<RabbitMqIntegrationListenerService> logger,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_handler = handler;
|
||||
_routingKey = routingKey;
|
||||
@ -35,6 +37,7 @@ public class RabbitMqIntegrationListenerService : BackgroundService
|
||||
_queueName = queueName;
|
||||
_rabbitMqService = rabbitMqService;
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider;
|
||||
_maxRetries = maxRetries;
|
||||
_lazyChannel = new Lazy<Task<IChannel>>(() => _rabbitMqService.CreateChannelAsync());
|
||||
}
|
||||
@ -74,7 +77,7 @@ public class RabbitMqIntegrationListenerService : BackgroundService
|
||||
var integrationMessage = JsonSerializer.Deserialize<IntegrationMessage>(json);
|
||||
if (integrationMessage is not null &&
|
||||
integrationMessage.DelayUntilDate.HasValue &&
|
||||
integrationMessage.DelayUntilDate.Value > DateTime.UtcNow)
|
||||
integrationMessage.DelayUntilDate.Value > _timeProvider.GetUtcNow().UtcDateTime)
|
||||
{
|
||||
await _rabbitMqService.RepublishToRetryQueueAsync(channel, ea);
|
||||
await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);
|
||||
|
@ -9,7 +9,9 @@ using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public class WebhookIntegrationHandler(IHttpClientFactory httpClientFactory)
|
||||
public class WebhookIntegrationHandler(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
TimeProvider timeProvider)
|
||||
: IntegrationHandlerBase<WebhookIntegrationConfigurationDetails>
|
||||
{
|
||||
private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);
|
||||
@ -39,7 +41,7 @@ public class WebhookIntegrationHandler(IHttpClientFactory httpClientFactory)
|
||||
if (int.TryParse(value, out var seconds))
|
||||
{
|
||||
// Retry-after was specified in seconds. Adjust DelayUntilDate by the requested number of seconds.
|
||||
result.DelayUntilDate = DateTime.UtcNow.AddSeconds(seconds);
|
||||
result.DelayUntilDate = timeProvider.GetUtcNow().AddSeconds(seconds).UtcDateTime;
|
||||
}
|
||||
else if (DateTimeOffset.TryParseExact(value,
|
||||
"r", // "r" is the round-trip format: RFC1123
|
||||
|
@ -138,6 +138,7 @@ public static class FeatureFlagKeys
|
||||
public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill";
|
||||
public const string MacOsNativeCredentialSync = "macos-native-credential-sync";
|
||||
public const string InlineMenuTotp = "inline-menu-totp";
|
||||
public const string WindowsDesktopAutotype = "windows-desktop-autotype";
|
||||
|
||||
/* Billing Team */
|
||||
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
||||
@ -206,6 +207,7 @@ public static class FeatureFlagKeys
|
||||
public const string EndUserNotifications = "pm-10609-end-user-notifications";
|
||||
public const string PhishingDetection = "phishing-detection";
|
||||
public const string RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy";
|
||||
public const string PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
|
@ -4,22 +4,22 @@ using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Api;
|
||||
|
||||
public class PushSendRequestModel : IValidatableObject
|
||||
public class PushSendRequestModel<T> : IValidatableObject
|
||||
{
|
||||
public string? UserId { get; set; }
|
||||
public string? OrganizationId { get; set; }
|
||||
public string? DeviceId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public Guid? OrganizationId { get; set; }
|
||||
public Guid? DeviceId { get; set; }
|
||||
public string? Identifier { get; set; }
|
||||
public required PushType Type { get; set; }
|
||||
public required object Payload { get; set; }
|
||||
public required T Payload { get; set; }
|
||||
public ClientType? ClientType { get; set; }
|
||||
public string? InstallationId { get; set; }
|
||||
public Guid? InstallationId { get; set; }
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(UserId) &&
|
||||
string.IsNullOrWhiteSpace(OrganizationId) &&
|
||||
string.IsNullOrWhiteSpace(InstallationId))
|
||||
if (!UserId.HasValue &&
|
||||
!OrganizationId.HasValue &&
|
||||
!InstallationId.HasValue)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
$"{nameof(UserId)} or {nameof(OrganizationId)} or {nameof(InstallationId)} is required.");
|
||||
|
@ -1,21 +1,17 @@
|
||||
#nullable enable
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
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.Models.Data;
|
||||
using Bit.Core.NotificationCenter.Entities;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Platform.Push.Internal;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
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;
|
||||
|
||||
@ -26,52 +22,32 @@ namespace Bit.Core.NotificationHub;
|
||||
/// Used by Cloud-Hosted environments.
|
||||
/// Received by Firebase for Android or APNS for iOS.
|
||||
/// </summary>
|
||||
public class NotificationHubPushNotificationService : IPushNotificationService
|
||||
public class NotificationHubPushNotificationService : IPushEngine, IPushRelayer
|
||||
{
|
||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly bool _enableTracing = false;
|
||||
private readonly INotificationHubPool _notificationHubPool;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public NotificationHubPushNotificationService(
|
||||
IInstallationDeviceRepository installationDeviceRepository,
|
||||
INotificationHubPool notificationHubPool,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<NotificationHubPushNotificationService> logger,
|
||||
IGlobalSettings globalSettings,
|
||||
TimeProvider timeProvider)
|
||||
IGlobalSettings globalSettings)
|
||||
{
|
||||
_installationDeviceRepository = installationDeviceRepository;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_notificationHubPool = notificationHubPool;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
_timeProvider = timeProvider;
|
||||
if (globalSettings.Installation.Id == Guid.Empty)
|
||||
{
|
||||
logger.LogWarning("Installation ID is not set. Push notifications for installations will not work.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
{
|
||||
await PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds);
|
||||
}
|
||||
|
||||
public async Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
{
|
||||
await PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds);
|
||||
}
|
||||
|
||||
public async Task PushSyncCipherDeleteAsync(Cipher cipher)
|
||||
{
|
||||
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
|
||||
}
|
||||
|
||||
private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
|
||||
public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
|
||||
{
|
||||
if (cipher.OrganizationId.HasValue)
|
||||
{
|
||||
@ -93,311 +69,17 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
||||
CollectionIds = collectionIds,
|
||||
};
|
||||
|
||||
await SendPayloadToUserAsync(cipher.UserId.Value, type, message, true);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PushSyncFolderCreateAsync(Folder folder)
|
||||
{
|
||||
await PushFolderAsync(folder, PushType.SyncFolderCreate);
|
||||
}
|
||||
|
||||
public async Task PushSyncFolderUpdateAsync(Folder folder)
|
||||
{
|
||||
await PushFolderAsync(folder, PushType.SyncFolderUpdate);
|
||||
}
|
||||
|
||||
public async Task PushSyncFolderDeleteAsync(Folder folder)
|
||||
{
|
||||
await PushFolderAsync(folder, PushType.SyncFolderDelete);
|
||||
}
|
||||
|
||||
private async Task PushFolderAsync(Folder folder, PushType type)
|
||||
{
|
||||
var message = new SyncFolderPushNotification
|
||||
{
|
||||
Id = folder.Id,
|
||||
UserId = folder.UserId,
|
||||
RevisionDate = folder.RevisionDate
|
||||
};
|
||||
|
||||
await SendPayloadToUserAsync(folder.UserId, type, message, true);
|
||||
}
|
||||
|
||||
public async Task PushSyncCiphersAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncCiphers);
|
||||
}
|
||||
|
||||
public async Task PushSyncVaultAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncVault);
|
||||
}
|
||||
|
||||
public async Task PushSyncOrganizationsAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncOrganizations);
|
||||
}
|
||||
|
||||
public async Task PushSyncOrgKeysAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncOrgKeys);
|
||||
}
|
||||
|
||||
public async Task PushSyncSettingsAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncSettings);
|
||||
}
|
||||
|
||||
public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.LogOut, excludeCurrentContext);
|
||||
}
|
||||
|
||||
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
|
||||
{
|
||||
var message = new UserPushNotification { UserId = userId, Date = _timeProvider.GetUtcNow().UtcDateTime };
|
||||
|
||||
await SendPayloadToUserAsync(userId, type, message, excludeCurrentContext);
|
||||
}
|
||||
|
||||
public async Task PushSyncSendCreateAsync(Send send)
|
||||
{
|
||||
await PushSendAsync(send, PushType.SyncSendCreate);
|
||||
}
|
||||
|
||||
public async Task PushSyncSendUpdateAsync(Send send)
|
||||
{
|
||||
await PushSendAsync(send, PushType.SyncSendUpdate);
|
||||
}
|
||||
|
||||
public async Task PushSyncSendDeleteAsync(Send send)
|
||||
{
|
||||
await PushSendAsync(send, PushType.SyncSendDelete);
|
||||
}
|
||||
|
||||
private async Task PushSendAsync(Send send, PushType type)
|
||||
{
|
||||
if (send.UserId.HasValue)
|
||||
{
|
||||
var message = new SyncSendPushNotification
|
||||
await PushAsync(new PushNotification<SyncCipherPushNotification>
|
||||
{
|
||||
Id = send.Id,
|
||||
UserId = send.UserId.Value,
|
||||
RevisionDate = send.RevisionDate
|
||||
};
|
||||
|
||||
await SendPayloadToUserAsync(message.UserId, type, message, true);
|
||||
Type = type,
|
||||
Target = NotificationTarget.User,
|
||||
TargetId = cipher.UserId.Value,
|
||||
Payload = message,
|
||||
ExcludeCurrentContext = true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PushAuthRequestAsync(AuthRequest authRequest)
|
||||
{
|
||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequest);
|
||||
}
|
||||
|
||||
public async Task PushAuthRequestResponseAsync(AuthRequest authRequest)
|
||||
{
|
||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
|
||||
}
|
||||
|
||||
public async Task PushNotificationAsync(Notification notification)
|
||||
{
|
||||
Guid? installationId = notification.Global && _globalSettings.Installation.Id != Guid.Empty
|
||||
? _globalSettings.Installation.Id
|
||||
: null;
|
||||
|
||||
var message = new NotificationPushNotification
|
||||
{
|
||||
Id = notification.Id,
|
||||
Priority = notification.Priority,
|
||||
Global = notification.Global,
|
||||
ClientType = notification.ClientType,
|
||||
UserId = notification.UserId,
|
||||
OrganizationId = notification.OrganizationId,
|
||||
InstallationId = installationId,
|
||||
TaskId = notification.TaskId,
|
||||
Title = notification.Title,
|
||||
Body = notification.Body,
|
||||
CreationDate = notification.CreationDate,
|
||||
RevisionDate = notification.RevisionDate
|
||||
};
|
||||
|
||||
if (notification.Global)
|
||||
{
|
||||
if (installationId.HasValue)
|
||||
{
|
||||
await SendPayloadToInstallationAsync(installationId.Value, PushType.Notification, message, true,
|
||||
notification.ClientType);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Invalid global notification id {NotificationId} push notification. No installation id provided.",
|
||||
notification.Id);
|
||||
}
|
||||
}
|
||||
else if (notification.UserId.HasValue)
|
||||
{
|
||||
await SendPayloadToUserAsync(notification.UserId.Value, PushType.Notification, message, true,
|
||||
notification.ClientType);
|
||||
}
|
||||
else if (notification.OrganizationId.HasValue)
|
||||
{
|
||||
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.Notification, message,
|
||||
true, notification.ClientType);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Invalid notification id {NotificationId} push notification", notification.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
|
||||
{
|
||||
Guid? installationId = notification.Global && _globalSettings.Installation.Id != Guid.Empty
|
||||
? _globalSettings.Installation.Id
|
||||
: null;
|
||||
|
||||
var message = new NotificationPushNotification
|
||||
{
|
||||
Id = notification.Id,
|
||||
Priority = notification.Priority,
|
||||
Global = notification.Global,
|
||||
ClientType = notification.ClientType,
|
||||
UserId = notification.UserId,
|
||||
OrganizationId = notification.OrganizationId,
|
||||
InstallationId = installationId,
|
||||
TaskId = notification.TaskId,
|
||||
Title = notification.Title,
|
||||
Body = notification.Body,
|
||||
CreationDate = notification.CreationDate,
|
||||
RevisionDate = notification.RevisionDate,
|
||||
ReadDate = notificationStatus.ReadDate,
|
||||
DeletedDate = notificationStatus.DeletedDate
|
||||
};
|
||||
|
||||
if (notification.Global)
|
||||
{
|
||||
if (installationId.HasValue)
|
||||
{
|
||||
await SendPayloadToInstallationAsync(installationId.Value, PushType.NotificationStatus, message, true,
|
||||
notification.ClientType);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Invalid global notification status id {NotificationId} push notification. No installation id provided.",
|
||||
notification.Id);
|
||||
}
|
||||
}
|
||||
else if (notification.UserId.HasValue)
|
||||
{
|
||||
await SendPayloadToUserAsync(notification.UserId.Value, PushType.NotificationStatus, message, true,
|
||||
notification.ClientType);
|
||||
}
|
||||
else if (notification.OrganizationId.HasValue)
|
||||
{
|
||||
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.NotificationStatus,
|
||||
message, true, notification.ClientType);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Invalid notification status id {NotificationId} push notification", notification.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
|
||||
{
|
||||
var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId };
|
||||
|
||||
await SendPayloadToUserAsync(authRequest.UserId, type, message, true);
|
||||
}
|
||||
|
||||
private async Task SendPayloadToInstallationAsync(Guid installationId, PushType type, object payload,
|
||||
bool excludeCurrentContext, ClientType? clientType = null)
|
||||
{
|
||||
await SendPayloadToInstallationAsync(installationId.ToString(), type, payload,
|
||||
GetContextIdentifier(excludeCurrentContext), clientType: clientType);
|
||||
}
|
||||
|
||||
private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext,
|
||||
ClientType? clientType = null)
|
||||
{
|
||||
await SendPayloadToUserAsync(userId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext),
|
||||
clientType: clientType);
|
||||
}
|
||||
|
||||
private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload,
|
||||
bool excludeCurrentContext, ClientType? clientType = null)
|
||||
{
|
||||
await SendPayloadToOrganizationAsync(orgId.ToString(), type, payload,
|
||||
GetContextIdentifier(excludeCurrentContext), clientType: clientType);
|
||||
}
|
||||
|
||||
public async Task PushPendingSecurityTasksAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.PendingSecurityTasks);
|
||||
}
|
||||
|
||||
public async Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload,
|
||||
string? identifier, string? deviceId = null, ClientType? clientType = null)
|
||||
{
|
||||
var tag = BuildTag($"template:payload && installationId:{installationId}", identifier, clientType);
|
||||
await SendPayloadAsync(tag, type, payload);
|
||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
||||
{
|
||||
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
|
||||
}
|
||||
}
|
||||
|
||||
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, clientType);
|
||||
await SendPayloadAsync(tag, type, payload);
|
||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
||||
{
|
||||
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
|
||||
}
|
||||
}
|
||||
|
||||
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, clientType);
|
||||
await SendPayloadAsync(tag, type, payload);
|
||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
||||
{
|
||||
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PushSyncOrganizationStatusAsync(Organization organization)
|
||||
{
|
||||
var message = new OrganizationStatusPushNotification
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Enabled = organization.Enabled
|
||||
};
|
||||
|
||||
await SendPayloadToOrganizationAsync(organization.Id, PushType.SyncOrganizationStatusChanged, message, false);
|
||||
}
|
||||
|
||||
public async Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) =>
|
||||
await SendPayloadToOrganizationAsync(
|
||||
organization.Id,
|
||||
PushType.SyncOrganizationCollectionSettingChanged,
|
||||
new OrganizationCollectionManagementPushNotification
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation,
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion,
|
||||
LimitItemDeletion = organization.LimitItemDeletion
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
private string? GetContextIdentifier(bool excludeCurrentContext)
|
||||
{
|
||||
if (!excludeCurrentContext)
|
||||
@ -425,13 +107,73 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
||||
return $"({tag})";
|
||||
}
|
||||
|
||||
private async Task SendPayloadAsync(string tag, PushType type, object payload)
|
||||
public async Task PushAsync<T>(PushNotification<T> pushNotification)
|
||||
where T : class
|
||||
{
|
||||
var initialTag = pushNotification.Target switch
|
||||
{
|
||||
NotificationTarget.User => $"template:payload_userId:{pushNotification.TargetId}",
|
||||
NotificationTarget.Organization => $"template:payload && organizationId:{pushNotification.TargetId}",
|
||||
NotificationTarget.Installation => $"template:payload && installationId:{pushNotification.TargetId}",
|
||||
_ => throw new InvalidOperationException($"Push notification target '{pushNotification.Target}' is not valid."),
|
||||
};
|
||||
|
||||
await PushCoreAsync(
|
||||
initialTag,
|
||||
GetContextIdentifier(pushNotification.ExcludeCurrentContext),
|
||||
pushNotification.Type,
|
||||
pushNotification.ClientType,
|
||||
pushNotification.Payload
|
||||
);
|
||||
}
|
||||
|
||||
public async Task RelayAsync(Guid fromInstallation, RelayedNotification relayedNotification)
|
||||
{
|
||||
// Relayed notifications need identifiers prefixed with the installation they are from and a underscore
|
||||
var initialTag = relayedNotification.Target switch
|
||||
{
|
||||
NotificationTarget.User => $"template:payload_userId:{fromInstallation}_{relayedNotification.TargetId}",
|
||||
NotificationTarget.Organization => $"template:payload && organizationId:{fromInstallation}_{relayedNotification.TargetId}",
|
||||
NotificationTarget.Installation => $"template:payload && installationId:{fromInstallation}",
|
||||
_ => throw new InvalidOperationException($"Invalid Notification target {relayedNotification.Target}"),
|
||||
};
|
||||
|
||||
await PushCoreAsync(
|
||||
initialTag,
|
||||
relayedNotification.Identifier,
|
||||
relayedNotification.Type,
|
||||
relayedNotification.ClientType,
|
||||
relayedNotification.Payload
|
||||
);
|
||||
|
||||
if (relayedNotification.DeviceId.HasValue)
|
||||
{
|
||||
await _installationDeviceRepository.UpsertAsync(
|
||||
new InstallationDeviceEntity(fromInstallation, relayedNotification.DeviceId.Value)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"A related notification of type '{Type}' came through without a device id from installation {Installation}",
|
||||
relayedNotification.Type,
|
||||
fromInstallation
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PushCoreAsync<T>(string initialTag, string? contextId, PushType pushType, ClientType? clientType, T payload)
|
||||
{
|
||||
var finalTag = BuildTag(initialTag, contextId, clientType);
|
||||
|
||||
var results = await _notificationHubPool.AllClients.SendTemplateNotificationAsync(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "type", ((byte)type).ToString() }, { "payload", JsonSerializer.Serialize(payload) }
|
||||
}, tag);
|
||||
{ "type", ((byte)pushType).ToString() },
|
||||
{ "payload", JsonSerializer.Serialize(payload) },
|
||||
},
|
||||
finalTag
|
||||
);
|
||||
|
||||
if (_enableTracing)
|
||||
{
|
||||
@ -444,7 +186,7 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
||||
|
||||
_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);
|
||||
outcome.TrackingId, pushType, outcome.Success, outcome.Failure, payload, outcome.Results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,10 @@
|
||||
#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.NotificationCenter.Entities;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@ -17,12 +13,10 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Platform.Push.Internal;
|
||||
|
||||
public class AzureQueuePushNotificationService : IPushNotificationService
|
||||
public class AzureQueuePushNotificationService : IPushEngine
|
||||
{
|
||||
private readonly QueueClient _queueClient;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public AzureQueuePushNotificationService(
|
||||
[FromKeyedServices("notifications")] QueueClient queueClient,
|
||||
@ -33,30 +27,13 @@ public class AzureQueuePushNotificationService : IPushNotificationService
|
||||
{
|
||||
_queueClient = queueClient;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_globalSettings = globalSettings;
|
||||
_timeProvider = timeProvider;
|
||||
if (globalSettings.Installation.Id == Guid.Empty)
|
||||
{
|
||||
logger.LogWarning("Installation ID is not set. Push notifications for installations will not work.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
{
|
||||
await PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds);
|
||||
}
|
||||
|
||||
public async Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
{
|
||||
await PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds);
|
||||
}
|
||||
|
||||
public async Task PushSyncCipherDeleteAsync(Cipher cipher)
|
||||
{
|
||||
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
|
||||
}
|
||||
|
||||
private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
|
||||
public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
|
||||
{
|
||||
if (cipher.OrganizationId.HasValue)
|
||||
{
|
||||
@ -83,166 +60,6 @@ public class AzureQueuePushNotificationService : IPushNotificationService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PushSyncFolderCreateAsync(Folder folder)
|
||||
{
|
||||
await PushFolderAsync(folder, PushType.SyncFolderCreate);
|
||||
}
|
||||
|
||||
public async Task PushSyncFolderUpdateAsync(Folder folder)
|
||||
{
|
||||
await PushFolderAsync(folder, PushType.SyncFolderUpdate);
|
||||
}
|
||||
|
||||
public async Task PushSyncFolderDeleteAsync(Folder folder)
|
||||
{
|
||||
await PushFolderAsync(folder, PushType.SyncFolderDelete);
|
||||
}
|
||||
|
||||
private async Task PushFolderAsync(Folder folder, PushType type)
|
||||
{
|
||||
var message = new SyncFolderPushNotification
|
||||
{
|
||||
Id = folder.Id,
|
||||
UserId = folder.UserId,
|
||||
RevisionDate = folder.RevisionDate
|
||||
};
|
||||
|
||||
await SendMessageAsync(type, message, true);
|
||||
}
|
||||
|
||||
public async Task PushSyncCiphersAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncCiphers);
|
||||
}
|
||||
|
||||
public async Task PushSyncVaultAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncVault);
|
||||
}
|
||||
|
||||
public async Task PushSyncOrganizationsAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncOrganizations);
|
||||
}
|
||||
|
||||
public async Task PushSyncOrgKeysAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncOrgKeys);
|
||||
}
|
||||
|
||||
public async Task PushSyncSettingsAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncSettings);
|
||||
}
|
||||
|
||||
public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.LogOut, excludeCurrentContext);
|
||||
}
|
||||
|
||||
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
|
||||
{
|
||||
var message = new UserPushNotification { UserId = userId, Date = _timeProvider.GetUtcNow().UtcDateTime };
|
||||
|
||||
await SendMessageAsync(type, message, excludeCurrentContext);
|
||||
}
|
||||
|
||||
public async Task PushAuthRequestAsync(AuthRequest authRequest)
|
||||
{
|
||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequest);
|
||||
}
|
||||
|
||||
public async Task PushAuthRequestResponseAsync(AuthRequest authRequest)
|
||||
{
|
||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
|
||||
}
|
||||
|
||||
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
|
||||
{
|
||||
var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId };
|
||||
|
||||
await SendMessageAsync(type, message, true);
|
||||
}
|
||||
|
||||
public async Task PushSyncSendCreateAsync(Send send)
|
||||
{
|
||||
await PushSendAsync(send, PushType.SyncSendCreate);
|
||||
}
|
||||
|
||||
public async Task PushSyncSendUpdateAsync(Send send)
|
||||
{
|
||||
await PushSendAsync(send, PushType.SyncSendUpdate);
|
||||
}
|
||||
|
||||
public async Task PushSyncSendDeleteAsync(Send send)
|
||||
{
|
||||
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,
|
||||
InstallationId = notification.Global ? _globalSettings.Installation.Id : null,
|
||||
TaskId = notification.TaskId,
|
||||
Title = notification.Title,
|
||||
Body = notification.Body,
|
||||
CreationDate = notification.CreationDate,
|
||||
RevisionDate = notification.RevisionDate
|
||||
};
|
||||
|
||||
await SendMessageAsync(PushType.Notification, 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,
|
||||
InstallationId = notification.Global ? _globalSettings.Installation.Id : null,
|
||||
TaskId = notification.TaskId,
|
||||
Title = notification.Title,
|
||||
Body = notification.Body,
|
||||
CreationDate = notification.CreationDate,
|
||||
RevisionDate = notification.RevisionDate,
|
||||
ReadDate = notificationStatus.ReadDate,
|
||||
DeletedDate = notificationStatus.DeletedDate
|
||||
};
|
||||
|
||||
await SendMessageAsync(PushType.NotificationStatus, message, true);
|
||||
}
|
||||
|
||||
public async Task PushPendingSecurityTasksAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.PendingSecurityTasks);
|
||||
}
|
||||
|
||||
private async Task PushSendAsync(Send send, PushType type)
|
||||
{
|
||||
if (send.UserId.HasValue)
|
||||
{
|
||||
var message = new SyncSendPushNotification
|
||||
{
|
||||
Id = send.Id,
|
||||
UserId = send.UserId.Value,
|
||||
RevisionDate = send.RevisionDate
|
||||
};
|
||||
|
||||
await SendMessageAsync(type, message, true);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendMessageAsync<T>(PushType type, T payload, bool excludeCurrentContext)
|
||||
{
|
||||
var contextId = GetContextIdentifier(excludeCurrentContext);
|
||||
@ -263,42 +80,9 @@ public class AzureQueuePushNotificationService : IPushNotificationService
|
||||
return currentContext?.DeviceIdentifier;
|
||||
}
|
||||
|
||||
public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier,
|
||||
string? deviceId = null, ClientType? clientType = null) =>
|
||||
// Noop
|
||||
Task.CompletedTask;
|
||||
|
||||
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
|
||||
string? deviceId = null, ClientType? clientType = null)
|
||||
public async Task PushAsync<T>(PushNotification<T> pushNotification)
|
||||
where T : class
|
||||
{
|
||||
// Noop
|
||||
return Task.FromResult(0);
|
||||
await SendMessageAsync(pushNotification.Type, pushNotification.Payload, pushNotification.ExcludeCurrentContext);
|
||||
}
|
||||
|
||||
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
|
||||
string? deviceId = null, ClientType? clientType = null)
|
||||
{
|
||||
// Noop
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public async Task PushSyncOrganizationStatusAsync(Organization organization)
|
||||
{
|
||||
var message = new OrganizationStatusPushNotification
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Enabled = organization.Enabled
|
||||
};
|
||||
await SendMessageAsync(PushType.SyncOrganizationStatusChanged, message, false);
|
||||
}
|
||||
|
||||
public async Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) =>
|
||||
await SendMessageAsync(PushType.SyncOrganizationCollectionSettingChanged,
|
||||
new OrganizationCollectionManagementPushNotification
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation,
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion,
|
||||
LimitItemDeletion = organization.LimitItemDeletion
|
||||
}, false);
|
||||
}
|
||||
|
13
src/Core/Platform/Push/Services/IPushEngine.cs
Normal file
13
src/Core/Platform/Push/Services/IPushEngine.cs
Normal file
@ -0,0 +1,13 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Vault.Entities;
|
||||
|
||||
namespace Bit.Core.Platform.Push;
|
||||
|
||||
public interface IPushEngine
|
||||
{
|
||||
Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable<Guid>? collectionIds);
|
||||
|
||||
Task PushAsync<T>(PushNotification<T> pushNotification)
|
||||
where T : class;
|
||||
}
|
@ -2,41 +2,410 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.NotificationCenter.Entities;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Platform.Push;
|
||||
|
||||
public interface IPushNotificationService
|
||||
{
|
||||
Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds);
|
||||
Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds);
|
||||
Task PushSyncCipherDeleteAsync(Cipher cipher);
|
||||
Task PushSyncFolderCreateAsync(Folder folder);
|
||||
Task PushSyncFolderUpdateAsync(Folder folder);
|
||||
Task PushSyncFolderDeleteAsync(Folder folder);
|
||||
Task PushSyncCiphersAsync(Guid userId);
|
||||
Task PushSyncVaultAsync(Guid userId);
|
||||
Task PushSyncOrganizationsAsync(Guid userId);
|
||||
Task PushSyncOrgKeysAsync(Guid userId);
|
||||
Task PushSyncSettingsAsync(Guid userId);
|
||||
Task PushLogOutAsync(Guid userId, bool excludeCurrentContextFromPush = false);
|
||||
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 PushSyncOrganizationStatusAsync(Organization organization);
|
||||
Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization);
|
||||
Guid InstallationId { get; }
|
||||
TimeProvider TimeProvider { get; }
|
||||
ILogger Logger { get; }
|
||||
|
||||
Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier,
|
||||
string? deviceId = null, ClientType? clientType = null);
|
||||
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);
|
||||
Task PushPendingSecurityTasksAsync(Guid userId);
|
||||
#region Legacy method, to be removed soon.
|
||||
Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
=> PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds);
|
||||
|
||||
Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
=> PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds);
|
||||
|
||||
Task PushSyncCipherDeleteAsync(Cipher cipher)
|
||||
=> PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
|
||||
|
||||
Task PushSyncFolderCreateAsync(Folder folder)
|
||||
=> PushAsync(new PushNotification<SyncFolderPushNotification>
|
||||
{
|
||||
Type = PushType.SyncFolderCreate,
|
||||
Target = NotificationTarget.User,
|
||||
TargetId = folder.UserId,
|
||||
Payload = new SyncFolderPushNotification
|
||||
{
|
||||
Id = folder.Id,
|
||||
UserId = folder.UserId,
|
||||
RevisionDate = folder.RevisionDate,
|
||||
},
|
||||
ExcludeCurrentContext = true,
|
||||
});
|
||||
|
||||
Task PushSyncFolderUpdateAsync(Folder folder)
|
||||
=> PushAsync(new PushNotification<SyncFolderPushNotification>
|
||||
{
|
||||
Type = PushType.SyncFolderUpdate,
|
||||
Target = NotificationTarget.User,
|
||||
TargetId = folder.UserId,
|
||||
Payload = new SyncFolderPushNotification
|
||||
{
|
||||
Id = folder.Id,
|
||||
UserId = folder.UserId,
|
||||
RevisionDate = folder.RevisionDate,
|
||||
},
|
||||
ExcludeCurrentContext = true,
|
||||
});
|
||||
|
||||
Task PushSyncFolderDeleteAsync(Folder folder)
|
||||
=> PushAsync(new PushNotification<SyncFolderPushNotification>
|
||||
{
|
||||
Type = PushType.SyncFolderDelete,
|
||||
Target = NotificationTarget.User,
|
||||
TargetId = folder.UserId,
|
||||
Payload = new SyncFolderPushNotification
|
||||
{
|
||||
Id = folder.Id,
|
||||
UserId = folder.UserId,
|
||||
RevisionDate = folder.RevisionDate,
|
||||
},
|
||||
ExcludeCurrentContext = true,
|
||||
});
|
||||
|
||||
Task PushSyncCiphersAsync(Guid userId)
|
||||
=> PushAsync(new PushNotification<UserPushNotification>
|
||||
{
|
||||
Type = PushType.SyncCiphers,
|
||||
Target = NotificationTarget.User,
|
||||
TargetId = userId,
|
||||
Payload = new UserPushNotification
|
||||
{
|
||||
UserId = userId,
|
||||
Date = TimeProvider.GetUtcNow().UtcDateTime,
|
||||
},
|
||||
ExcludeCurrentContext = false,
|
||||
});
|
||||
|
||||
Task PushSyncVaultAsync(Guid userId)
|
||||
=> PushAsync(new PushNotification<UserPushNotification>
|
||||
{
|
||||
Type = PushType.SyncVault,
|
||||
Target = NotificationTarget.User,
|
||||
TargetId = userId,
|
||||
Payload = new UserPushNotification
|
||||
{
|
||||
UserId = userId,
|
||||
Date = TimeProvider.GetUtcNow().UtcDateTime,
|
||||
},
|
||||
ExcludeCurrentContext = false,
|
||||
});
|
||||
|
||||
Task PushSyncOrganizationsAsync(Guid userId)
|
||||
=> PushAsync(new PushNotification<UserPushNotification>
|
||||
{
|
||||
Type = PushType.SyncOrganizations,
|
||||
Target = NotificationTarget.User,
|
||||
TargetId = userId,
|
||||
Payload = new UserPushNotification
|
||||
{
|
||||
UserId = userId,
|
||||
Date = TimeProvider.GetUtcNow().UtcDateTime,
|
||||
},
|
||||
ExcludeCurrentContext = false,
|
||||
});
|
||||
|
||||
Task PushSyncOrgKeysAsync(Guid userId)
|
||||
=> PushAsync(new PushNotification<UserPushNotification>
|
||||
{
|
||||
Type = PushType.SyncOrgKeys,
|
||||
Target = NotificationTarget.User,
|
||||
TargetId = userId,
|
||||
Payload = new UserPushNotification
|
||||
{
|
||||
UserId = userId,
|
||||
Date = TimeProvider.GetUtcNow().UtcDateTime,
|
||||
},
|
||||
ExcludeCurrentContext = false,
|
||||
});
|
||||
|
||||
Task PushSyncSettingsAsync(Guid userId)
|
||||
=> PushAsync(new PushNotification<UserPushNotification>
|
||||
{
|
||||
Type = PushType.SyncSettings,
|
||||
Target = NotificationTarget.User,
|
||||
TargetId = userId,
|
||||
Payload = new UserPushNotification
|
||||
{
|
||||
UserId = userId,
|
||||
Date = TimeProvider.GetUtcNow().UtcDateTime,
|
||||
},
|
||||
ExcludeCurrentContext = false,
|
||||
});
|
||||
|
||||
Task PushLogOutAsync(Guid userId, bool excludeCurrentContextFromPush = false)
|
||||
=> PushAsync(new PushNotification<UserPushNotification>
|
||||
{
|
||||
Type = PushType.LogOut,
|
||||
Target = NotificationTarget.User,
|
||||
TargetId = userId,
|
||||
Payload = new UserPushNotification
|
||||
{
|
||||
UserId = userId,
|
||||
Date = TimeProvider.GetUtcNow().UtcDateTime,
|
||||
},
|
||||
ExcludeCurrentContext = excludeCurrentContextFromPush,
|
||||
});
|
||||
|
||||
Task PushSyncSendCreateAsync(Send send)
|
||||
{
|
||||
if (send.UserId.HasValue)
|
||||
{
|
||||
return PushAsync(new PushNotification<SyncSendPushNotification>
|
||||
{
|
||||
Type = PushType.SyncSendCreate,
|
||||
Target = NotificationTarget.User,
|
||||
TargetId = send.UserId.Value,
|
||||
Payload = new SyncSendPushNotification
|
||||
{
|
||||
Id = send.Id,
|
||||
UserId = send.UserId.Value,
|
||||
RevisionDate = send.RevisionDate,
|
||||
},
|
||||
ExcludeCurrentContext = true,
|
||||
});
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task PushSyncSendUpdateAsync(Send send)
|
||||
{
|
||||
if (send.UserId.HasValue)
|
||||
{
|
||||
return PushAsync(new PushNotification<SyncSendPushNotification>
|
||||
{
|
||||
Type = PushType.SyncSendUpdate,
|
||||
Target = NotificationTarget.User,
|
||||
TargetId = send.UserId.Value,
|
||||
Payload = new SyncSendPushNotification
|
||||
{
|
||||
Id = send.Id,
|
||||
UserId = send.UserId.Value,
|
||||
RevisionDate = send.RevisionDate,
|
||||
},
|
||||
ExcludeCurrentContext = true,
|
||||
});
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task PushSyncSendDeleteAsync(Send send)
|
||||
{
|
||||
if (send.UserId.HasValue)
|
||||
{
|
||||
return PushAsync(new PushNotification<SyncSendPushNotification>
|
||||
{
|
||||
Type = PushType.SyncSendDelete,
|
||||
Target = NotificationTarget.User,
|
||||
TargetId = send.UserId.Value,
|
||||
Payload = new SyncSendPushNotification
|
||||
{
|
||||
Id = send.Id,
|
||||
UserId = send.UserId.Value,
|
||||
RevisionDate = send.RevisionDate,
|
||||
},
|
||||
ExcludeCurrentContext = true,
|
||||
});
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
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,
|
||||
InstallationId = notification.Global ? InstallationId : null,
|
||||
TaskId = notification.TaskId,
|
||||
Title = notification.Title,
|
||||
Body = notification.Body,
|
||||
CreationDate = notification.CreationDate,
|
||||
RevisionDate = notification.RevisionDate,
|
||||
};
|
||||
|
||||
NotificationTarget target;
|
||||
Guid targetId;
|
||||
|
||||
if (notification.Global)
|
||||
{
|
||||
// TODO: Think about this a bit more
|
||||
target = NotificationTarget.Installation;
|
||||
targetId = InstallationId;
|
||||
}
|
||||
else if (notification.UserId.HasValue)
|
||||
{
|
||||
target = NotificationTarget.User;
|
||||
targetId = notification.UserId.Value;
|
||||
}
|
||||
else if (notification.OrganizationId.HasValue)
|
||||
{
|
||||
target = NotificationTarget.Organization;
|
||||
targetId = notification.OrganizationId.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("Invalid notification id {NotificationId} push notification", notification.Id);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return PushAsync(new PushNotification<NotificationPushNotification>
|
||||
{
|
||||
Type = PushType.Notification,
|
||||
Target = target,
|
||||
TargetId = targetId,
|
||||
Payload = message,
|
||||
ExcludeCurrentContext = true,
|
||||
ClientType = notification.ClientType,
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
InstallationId = notification.Global ? InstallationId : null,
|
||||
TaskId = notification.TaskId,
|
||||
Title = notification.Title,
|
||||
Body = notification.Body,
|
||||
CreationDate = notification.CreationDate,
|
||||
RevisionDate = notification.RevisionDate,
|
||||
ReadDate = notificationStatus.ReadDate,
|
||||
DeletedDate = notificationStatus.DeletedDate,
|
||||
};
|
||||
|
||||
NotificationTarget target;
|
||||
Guid targetId;
|
||||
|
||||
if (notification.Global)
|
||||
{
|
||||
// TODO: Think about this a bit more
|
||||
target = NotificationTarget.Installation;
|
||||
targetId = InstallationId;
|
||||
}
|
||||
else if (notification.UserId.HasValue)
|
||||
{
|
||||
target = NotificationTarget.User;
|
||||
targetId = notification.UserId.Value;
|
||||
}
|
||||
else if (notification.OrganizationId.HasValue)
|
||||
{
|
||||
target = NotificationTarget.Organization;
|
||||
targetId = notification.OrganizationId.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("Invalid notification status id {NotificationId} push notification", notification.Id);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return PushAsync(new PushNotification<NotificationPushNotification>
|
||||
{
|
||||
Type = PushType.NotificationStatus,
|
||||
Target = target,
|
||||
TargetId = targetId,
|
||||
Payload = message,
|
||||
ExcludeCurrentContext = true,
|
||||
ClientType = notification.ClientType,
|
||||
});
|
||||
}
|
||||
|
||||
Task PushAuthRequestAsync(AuthRequest authRequest)
|
||||
=> PushAsync(new PushNotification<AuthRequestPushNotification>
|
||||
{
|
||||
Type = PushType.AuthRequest,
|
||||
Target = NotificationTarget.User,
|
||||
TargetId = authRequest.UserId,
|
||||
Payload = new AuthRequestPushNotification
|
||||
{
|
||||
Id = authRequest.Id,
|
||||
UserId = authRequest.UserId,
|
||||
},
|
||||
ExcludeCurrentContext = true,
|
||||
});
|
||||
|
||||
Task PushAuthRequestResponseAsync(AuthRequest authRequest)
|
||||
=> PushAsync(new PushNotification<AuthRequestPushNotification>
|
||||
{
|
||||
Type = PushType.AuthRequestResponse,
|
||||
Target = NotificationTarget.User,
|
||||
TargetId = authRequest.UserId,
|
||||
Payload = new AuthRequestPushNotification
|
||||
{
|
||||
Id = authRequest.Id,
|
||||
UserId = authRequest.UserId,
|
||||
},
|
||||
ExcludeCurrentContext = true,
|
||||
});
|
||||
|
||||
Task PushSyncOrganizationStatusAsync(Organization organization)
|
||||
=> PushAsync(new PushNotification<OrganizationStatusPushNotification>
|
||||
{
|
||||
Type = PushType.SyncOrganizationStatusChanged,
|
||||
Target = NotificationTarget.Organization,
|
||||
TargetId = organization.Id,
|
||||
Payload = new OrganizationStatusPushNotification
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Enabled = organization.Enabled,
|
||||
},
|
||||
ExcludeCurrentContext = false,
|
||||
});
|
||||
|
||||
Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization)
|
||||
=> PushAsync(new PushNotification<OrganizationCollectionManagementPushNotification>
|
||||
{
|
||||
Type = PushType.SyncOrganizationCollectionSettingChanged,
|
||||
Target = NotificationTarget.Organization,
|
||||
TargetId = organization.Id,
|
||||
Payload = new OrganizationCollectionManagementPushNotification
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation,
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion,
|
||||
LimitItemDeletion = organization.LimitItemDeletion,
|
||||
},
|
||||
ExcludeCurrentContext = false,
|
||||
});
|
||||
|
||||
Task PushPendingSecurityTasksAsync(Guid userId)
|
||||
=> PushAsync(new PushNotification<UserPushNotification>
|
||||
{
|
||||
Type = PushType.PendingSecurityTasks,
|
||||
Target = NotificationTarget.User,
|
||||
TargetId = userId,
|
||||
Payload = new UserPushNotification
|
||||
{
|
||||
UserId = userId,
|
||||
Date = TimeProvider.GetUtcNow().UtcDateTime,
|
||||
},
|
||||
ExcludeCurrentContext = false,
|
||||
});
|
||||
#endregion
|
||||
|
||||
Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable<Guid>? collectionIds);
|
||||
|
||||
Task PushAsync<T>(PushNotification<T> pushNotification)
|
||||
where T : class;
|
||||
}
|
||||
|
44
src/Core/Platform/Push/Services/IPushRelayer.cs
Normal file
44
src/Core/Platform/Push/Services/IPushRelayer.cs
Normal file
@ -0,0 +1,44 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Platform.Push.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// An object encapsulating the information that is available in a notification
|
||||
/// given to us from a self-hosted installation.
|
||||
/// </summary>
|
||||
public class RelayedNotification
|
||||
{
|
||||
/// <inheritdoc cref="PushNotification{T}.Type"/>
|
||||
public required PushType Type { get; init; }
|
||||
/// <inheritdoc cref="PushNotification{T}.Target"/>
|
||||
public required NotificationTarget Target { get; init; }
|
||||
/// <inheritdoc cref="PushNotification{T}.TargetId"/>
|
||||
public required Guid TargetId { get; init; }
|
||||
/// <inheritdoc cref="PushNotification{T}.Payload"/>
|
||||
public required JsonElement Payload { get; init; }
|
||||
/// <inheritdoc cref="PushNotification{T}.ClientType"/>
|
||||
public required ClientType? ClientType { get; init; }
|
||||
public required Guid? DeviceId { get; init; }
|
||||
public required string? Identifier { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A service for taking a notification that was relayed to us from a self-hosted installation and
|
||||
/// will be injested into our infrastructure so that we can get the notification to devices that require
|
||||
/// cloud interaction.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This interface should be treated as internal and not consumed by other teams.
|
||||
/// </remarks>
|
||||
public interface IPushRelayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Relays a notification that was received from an authenticated installation into our cloud push notification infrastructure.
|
||||
/// </summary>
|
||||
/// <param name="fromInstallation">The authenticated installation this notification came from.</param>
|
||||
/// <param name="relayedNotification">The information received from the self-hosted installation.</param>
|
||||
Task RelayAsync(Guid fromInstallation, RelayedNotification relayedNotification);
|
||||
}
|
@ -1,202 +1,77 @@
|
||||
#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;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Platform.Push.Internal;
|
||||
|
||||
public class MultiServicePushNotificationService : IPushNotificationService
|
||||
{
|
||||
private readonly IEnumerable<IPushNotificationService> _services;
|
||||
private readonly ILogger<MultiServicePushNotificationService> _logger;
|
||||
private readonly IEnumerable<IPushEngine> _services;
|
||||
|
||||
public Guid InstallationId { get; }
|
||||
|
||||
public TimeProvider TimeProvider { get; }
|
||||
|
||||
public ILogger Logger { get; }
|
||||
|
||||
public MultiServicePushNotificationService(
|
||||
[FromKeyedServices("implementation")] IEnumerable<IPushNotificationService> services,
|
||||
IEnumerable<IPushEngine> services,
|
||||
ILogger<MultiServicePushNotificationService> logger,
|
||||
GlobalSettings globalSettings)
|
||||
GlobalSettings globalSettings,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_services = services;
|
||||
|
||||
_logger = logger;
|
||||
_logger.LogInformation("Hub services: {Services}", _services.Count());
|
||||
Logger = logger;
|
||||
Logger.LogInformation("Hub services: {Services}", _services.Count());
|
||||
globalSettings.NotificationHubPool?.NotificationHubs?.ForEach(hub =>
|
||||
{
|
||||
_logger.LogInformation("HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}", hub.HubName, hub.EnableSendTracing, hub.RegistrationStartDate, hub.RegistrationEndDate);
|
||||
Logger.LogInformation("HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}", hub.HubName, hub.EnableSendTracing, hub.RegistrationStartDate, hub.RegistrationEndDate);
|
||||
});
|
||||
InstallationId = globalSettings.Installation.Id;
|
||||
TimeProvider = timeProvider;
|
||||
}
|
||||
|
||||
public Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
{
|
||||
PushToServices((s) => s.PushSyncCipherCreateAsync(cipher, collectionIds));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
{
|
||||
PushToServices((s) => s.PushSyncCipherUpdateAsync(cipher, collectionIds));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncCipherDeleteAsync(Cipher cipher)
|
||||
{
|
||||
PushToServices((s) => s.PushSyncCipherDeleteAsync(cipher));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncFolderCreateAsync(Folder folder)
|
||||
{
|
||||
PushToServices((s) => s.PushSyncFolderCreateAsync(folder));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncFolderUpdateAsync(Folder folder)
|
||||
{
|
||||
PushToServices((s) => s.PushSyncFolderUpdateAsync(folder));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncFolderDeleteAsync(Folder folder)
|
||||
{
|
||||
PushToServices((s) => s.PushSyncFolderDeleteAsync(folder));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncCiphersAsync(Guid userId)
|
||||
{
|
||||
PushToServices((s) => s.PushSyncCiphersAsync(userId));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncVaultAsync(Guid userId)
|
||||
{
|
||||
PushToServices((s) => s.PushSyncVaultAsync(userId));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncOrganizationsAsync(Guid userId)
|
||||
{
|
||||
PushToServices((s) => s.PushSyncOrganizationsAsync(userId));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncOrgKeysAsync(Guid userId)
|
||||
{
|
||||
PushToServices((s) => s.PushSyncOrgKeysAsync(userId));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncSettingsAsync(Guid userId)
|
||||
{
|
||||
PushToServices((s) => s.PushSyncSettingsAsync(userId));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false)
|
||||
{
|
||||
PushToServices((s) => s.PushLogOutAsync(userId, excludeCurrentContext));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncSendCreateAsync(Send send)
|
||||
{
|
||||
PushToServices((s) => s.PushSyncSendCreateAsync(send));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncSendUpdateAsync(Send send)
|
||||
{
|
||||
PushToServices((s) => s.PushSyncSendUpdateAsync(send));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushAuthRequestAsync(AuthRequest authRequest)
|
||||
{
|
||||
PushToServices((s) => s.PushAuthRequestAsync(authRequest));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushAuthRequestResponseAsync(AuthRequest authRequest)
|
||||
{
|
||||
PushToServices((s) => s.PushAuthRequestResponseAsync(authRequest));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncSendDeleteAsync(Send send)
|
||||
{
|
||||
PushToServices((s) => s.PushSyncSendDeleteAsync(send));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncOrganizationStatusAsync(Organization organization)
|
||||
{
|
||||
PushToServices((s) => s.PushSyncOrganizationStatusAsync(organization));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization)
|
||||
{
|
||||
PushToServices(s => s.PushSyncOrganizationCollectionManagementSettingsAsync(organization));
|
||||
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 SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier,
|
||||
string? deviceId = null, ClientType? clientType = null)
|
||||
{
|
||||
PushToServices((s) =>
|
||||
s.SendPayloadToInstallationAsync(installationId, type, payload, identifier, deviceId, clientType));
|
||||
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);
|
||||
}
|
||||
|
||||
public Task PushPendingSecurityTasksAsync(Guid userId)
|
||||
{
|
||||
PushToServices((s) => s.PushPendingSecurityTasksAsync(userId));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void PushToServices(Func<IPushNotificationService, Task> pushFunc)
|
||||
private Task PushToServices(Func<IPushEngine, Task> pushFunc)
|
||||
{
|
||||
if (!_services.Any())
|
||||
{
|
||||
_logger.LogWarning("No services found to push notification");
|
||||
return;
|
||||
Logger.LogWarning("No services found to push notification");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
var tasks = new List<Task>();
|
||||
#endif
|
||||
|
||||
foreach (var service in _services)
|
||||
{
|
||||
_logger.LogDebug("Pushing notification to service {ServiceName}", service.GetType().Name);
|
||||
Logger.LogDebug("Pushing notification to service {ServiceName}", service.GetType().Name);
|
||||
#if DEBUG
|
||||
var task =
|
||||
#endif
|
||||
pushFunc(service);
|
||||
#if DEBUG
|
||||
tasks.Add(task);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
return Task.WhenAll(tasks);
|
||||
#else
|
||||
return Task.CompletedTask;
|
||||
#endif
|
||||
}
|
||||
|
||||
public Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable<Guid>? collectionIds)
|
||||
{
|
||||
return PushToServices((s) => s.PushCipherAsync(cipher, pushType, collectionIds));
|
||||
}
|
||||
public Task PushAsync<T>(PushNotification<T> pushNotification) where T : class
|
||||
{
|
||||
return PushToServices((s) => s.PushAsync(pushNotification));
|
||||
}
|
||||
}
|
||||
|
@ -1,129 +1,12 @@
|
||||
#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;
|
||||
|
||||
namespace Bit.Core.Platform.Push.Internal;
|
||||
|
||||
public class NoopPushNotificationService : IPushNotificationService
|
||||
internal class NoopPushNotificationService : IPushEngine
|
||||
{
|
||||
public Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
public Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable<Guid>? collectionIds) => Task.CompletedTask;
|
||||
|
||||
public Task PushSyncCipherDeleteAsync(Cipher cipher)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncCiphersAsync(Guid userId)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncFolderCreateAsync(Folder folder)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncFolderDeleteAsync(Folder folder)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncFolderUpdateAsync(Folder folder)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncOrganizationsAsync(Guid userId)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncOrgKeysAsync(Guid userId)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncSettingsAsync(Guid userId)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncVaultAsync(Guid userId)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncSendCreateAsync(Send send)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncSendDeleteAsync(Send send)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncSendUpdateAsync(Send send)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
|
||||
string? deviceId = null, ClientType? clientType = null)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncOrganizationStatusAsync(Organization organization)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) => Task.CompletedTask;
|
||||
|
||||
public Task PushAuthRequestAsync(AuthRequest authRequest)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushAuthRequestResponseAsync(AuthRequest authRequest)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushNotificationAsync(Notification notification) => Task.CompletedTask;
|
||||
|
||||
public Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) =>
|
||||
Task.CompletedTask;
|
||||
|
||||
public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier,
|
||||
string? deviceId = null, ClientType? clientType = null) => Task.CompletedTask;
|
||||
|
||||
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
|
||||
string? deviceId = null, ClientType? clientType = null)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushPendingSecurityTasksAsync(Guid userId)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
public Task PushAsync<T>(PushNotification<T> pushNotification) where T : class => Task.CompletedTask;
|
||||
}
|
||||
|
@ -1,13 +1,9 @@
|
||||
#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;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -20,18 +16,15 @@ namespace Bit.Core.Platform.Push;
|
||||
/// Used by Cloud-Hosted environments.
|
||||
/// Received by AzureQueueHostedService message receiver in Notifications project.
|
||||
/// </summary>
|
||||
public class NotificationsApiPushNotificationService : BaseIdentityClientService, IPushNotificationService
|
||||
public class NotificationsApiPushNotificationService : BaseIdentityClientService, IPushEngine
|
||||
{
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public NotificationsApiPushNotificationService(
|
||||
IHttpClientFactory httpFactory,
|
||||
GlobalSettings globalSettings,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<NotificationsApiPushNotificationService> logger,
|
||||
TimeProvider timeProvider)
|
||||
ILogger<NotificationsApiPushNotificationService> logger)
|
||||
: base(
|
||||
httpFactory,
|
||||
globalSettings.BaseServiceUri.InternalNotifications,
|
||||
@ -41,27 +34,10 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
|
||||
globalSettings.InternalIdentityKey,
|
||||
logger)
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_timeProvider = timeProvider;
|
||||
}
|
||||
|
||||
public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
{
|
||||
await PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds);
|
||||
}
|
||||
|
||||
public async Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
{
|
||||
await PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds);
|
||||
}
|
||||
|
||||
public async Task PushSyncCipherDeleteAsync(Cipher cipher)
|
||||
{
|
||||
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
|
||||
}
|
||||
|
||||
private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
|
||||
public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
|
||||
{
|
||||
if (cipher.OrganizationId.HasValue)
|
||||
{
|
||||
@ -89,174 +65,6 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PushSyncFolderCreateAsync(Folder folder)
|
||||
{
|
||||
await PushFolderAsync(folder, PushType.SyncFolderCreate);
|
||||
}
|
||||
|
||||
public async Task PushSyncFolderUpdateAsync(Folder folder)
|
||||
{
|
||||
await PushFolderAsync(folder, PushType.SyncFolderUpdate);
|
||||
}
|
||||
|
||||
public async Task PushSyncFolderDeleteAsync(Folder folder)
|
||||
{
|
||||
await PushFolderAsync(folder, PushType.SyncFolderDelete);
|
||||
}
|
||||
|
||||
private async Task PushFolderAsync(Folder folder, PushType type)
|
||||
{
|
||||
var message = new SyncFolderPushNotification
|
||||
{
|
||||
Id = folder.Id,
|
||||
UserId = folder.UserId,
|
||||
RevisionDate = folder.RevisionDate
|
||||
};
|
||||
|
||||
await SendMessageAsync(type, message, true);
|
||||
}
|
||||
|
||||
public async Task PushSyncCiphersAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncCiphers);
|
||||
}
|
||||
|
||||
public async Task PushSyncVaultAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncVault);
|
||||
}
|
||||
|
||||
public async Task PushSyncOrganizationsAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncOrganizations);
|
||||
}
|
||||
|
||||
public async Task PushSyncOrgKeysAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncOrgKeys);
|
||||
}
|
||||
|
||||
public async Task PushSyncSettingsAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncSettings);
|
||||
}
|
||||
|
||||
public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.LogOut, excludeCurrentContext);
|
||||
}
|
||||
|
||||
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
|
||||
{
|
||||
var message = new UserPushNotification
|
||||
{
|
||||
UserId = userId,
|
||||
Date = _timeProvider.GetUtcNow().UtcDateTime,
|
||||
};
|
||||
|
||||
await SendMessageAsync(type, message, excludeCurrentContext);
|
||||
}
|
||||
|
||||
public async Task PushAuthRequestAsync(AuthRequest authRequest)
|
||||
{
|
||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequest);
|
||||
}
|
||||
|
||||
public async Task PushAuthRequestResponseAsync(AuthRequest authRequest)
|
||||
{
|
||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
|
||||
}
|
||||
|
||||
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
|
||||
{
|
||||
var message = new AuthRequestPushNotification
|
||||
{
|
||||
Id = authRequest.Id,
|
||||
UserId = authRequest.UserId
|
||||
};
|
||||
|
||||
await SendMessageAsync(type, message, true);
|
||||
}
|
||||
|
||||
public async Task PushSyncSendCreateAsync(Send send)
|
||||
{
|
||||
await PushSendAsync(send, PushType.SyncSendCreate);
|
||||
}
|
||||
|
||||
public async Task PushSyncSendUpdateAsync(Send send)
|
||||
{
|
||||
await PushSendAsync(send, PushType.SyncSendUpdate);
|
||||
}
|
||||
|
||||
public async Task PushSyncSendDeleteAsync(Send send)
|
||||
{
|
||||
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,
|
||||
InstallationId = notification.Global ? _globalSettings.Installation.Id : null,
|
||||
TaskId = notification.TaskId,
|
||||
Title = notification.Title,
|
||||
Body = notification.Body,
|
||||
CreationDate = notification.CreationDate,
|
||||
RevisionDate = notification.RevisionDate
|
||||
};
|
||||
|
||||
await SendMessageAsync(PushType.Notification, 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,
|
||||
InstallationId = notification.Global ? _globalSettings.Installation.Id : null,
|
||||
TaskId = notification.TaskId,
|
||||
Title = notification.Title,
|
||||
Body = notification.Body,
|
||||
CreationDate = notification.CreationDate,
|
||||
RevisionDate = notification.RevisionDate,
|
||||
ReadDate = notificationStatus.ReadDate,
|
||||
DeletedDate = notificationStatus.DeletedDate
|
||||
};
|
||||
|
||||
await SendMessageAsync(PushType.NotificationStatus, message, true);
|
||||
}
|
||||
|
||||
public async Task PushPendingSecurityTasksAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.PendingSecurityTasks);
|
||||
}
|
||||
|
||||
private async Task PushSendAsync(Send send, PushType type)
|
||||
{
|
||||
if (send.UserId.HasValue)
|
||||
{
|
||||
var message = new SyncSendPushNotification
|
||||
{
|
||||
Id = send.Id,
|
||||
UserId = send.UserId.Value,
|
||||
RevisionDate = send.RevisionDate
|
||||
};
|
||||
|
||||
await SendMessageAsync(type, message, false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendMessageAsync<T>(PushType type, T payload, bool excludeCurrentContext)
|
||||
{
|
||||
var contextId = GetContextIdentifier(excludeCurrentContext);
|
||||
@ -276,43 +84,8 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
|
||||
return currentContext?.DeviceIdentifier;
|
||||
}
|
||||
|
||||
public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier,
|
||||
string? deviceId = null, ClientType? clientType = null) =>
|
||||
// Noop
|
||||
Task.CompletedTask;
|
||||
|
||||
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
|
||||
string? deviceId = null, ClientType? clientType = null)
|
||||
public async Task PushAsync<T>(PushNotification<T> pushNotification) where T : class
|
||||
{
|
||||
// Noop
|
||||
return Task.FromResult(0);
|
||||
await SendMessageAsync(pushNotification.Type, pushNotification.Payload, pushNotification.ExcludeCurrentContext);
|
||||
}
|
||||
|
||||
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
|
||||
string? deviceId = null, ClientType? clientType = null)
|
||||
{
|
||||
// Noop
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public async Task PushSyncOrganizationStatusAsync(Organization organization)
|
||||
{
|
||||
var message = new OrganizationStatusPushNotification
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Enabled = organization.Enabled
|
||||
};
|
||||
|
||||
await SendMessageAsync(PushType.SyncOrganizationStatusChanged, message, false);
|
||||
}
|
||||
|
||||
public async Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) =>
|
||||
await SendMessageAsync(PushType.SyncOrganizationCollectionSettingChanged,
|
||||
new OrganizationCollectionManagementPushNotification
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation,
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion,
|
||||
LimitItemDeletion = organization.LimitItemDeletion
|
||||
}, false);
|
||||
}
|
||||
|
78
src/Core/Platform/Push/Services/PushNotification.cs
Normal file
78
src/Core/Platform/Push/Services/PushNotification.cs
Normal file
@ -0,0 +1,78 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Platform.Push;
|
||||
|
||||
/// <summary>
|
||||
/// Contains constants for all the available targets for a given notification.
|
||||
/// </summary>
|
||||
public enum NotificationTarget
|
||||
{
|
||||
/// <summary>
|
||||
/// The target for the notification is a single user.
|
||||
/// </summary>
|
||||
User,
|
||||
/// <summary>
|
||||
/// The target for the notification are all the users in an organization.
|
||||
/// </summary>
|
||||
Organization,
|
||||
/// <summary>
|
||||
/// The target for the notification are all the organizations,
|
||||
/// and all the users in that organization for a installation.
|
||||
/// </summary>
|
||||
Installation,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An object containing all the information required for getting a notification
|
||||
/// to an end users device and the information you want available to that device.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the payload. This type is expected to be able to be roundtripped as JSON.</typeparam>
|
||||
public record PushNotification<T>
|
||||
where T : class
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="PushType"/> to be associated with the notification. This is used to route
|
||||
/// the notification to the correct handler on the client side. Be sure to use the correct payload
|
||||
/// type for the associated <see cref="PushType"/>.
|
||||
/// </summary>
|
||||
public required PushType Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The target entity type for the notification.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the target type is <see cref="NotificationTarget.User"/> the <see cref="TargetId"/>
|
||||
/// property is expected to be a users ID. When it is <see cref="NotificationTarget.Organization"/>
|
||||
/// it should be an organizations id. When it is a <see cref="NotificationTarget.Installation"/>
|
||||
/// it should be an installation id.
|
||||
/// </remarks>
|
||||
public required NotificationTarget Target { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The indentifier for the given <see cref="Target"/>.
|
||||
/// </summary>
|
||||
public required Guid TargetId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The payload to be sent with the notification. This object will be JSON serialized.
|
||||
/// </summary>
|
||||
public required T Payload { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When <see langword="true"/> the notification will not include the current context identifier on it, this
|
||||
/// means that the notification may get handled on the device that this notification could have originated from.
|
||||
/// </summary>
|
||||
public required bool ExcludeCurrentContext { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of clients the notification should be sent to, if <see langword="null"/> then
|
||||
/// <see cref="ClientType.All"/> is inferred.
|
||||
/// </summary>
|
||||
public ClientType? ClientType { get; init; }
|
||||
|
||||
internal Guid? GetTargetWhen(NotificationTarget notificationTarget)
|
||||
{
|
||||
return Target == notificationTarget ? TargetId : null;
|
||||
}
|
||||
}
|
@ -1,18 +1,15 @@
|
||||
#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;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Platform.Push.Internal;
|
||||
@ -22,20 +19,18 @@ namespace Bit.Core.Platform.Push.Internal;
|
||||
/// Used by Self-Hosted environments.
|
||||
/// Received by PushController endpoint in Api project.
|
||||
/// </summary>
|
||||
public class RelayPushNotificationService : BaseIdentityClientService, IPushNotificationService
|
||||
public class RelayPushNotificationService : BaseIdentityClientService, IPushEngine
|
||||
{
|
||||
private readonly IDeviceRepository _deviceRepository;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
|
||||
public RelayPushNotificationService(
|
||||
IHttpClientFactory httpFactory,
|
||||
IDeviceRepository deviceRepository,
|
||||
GlobalSettings globalSettings,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<RelayPushNotificationService> logger,
|
||||
TimeProvider timeProvider)
|
||||
ILogger<RelayPushNotificationService> logger)
|
||||
: base(
|
||||
httpFactory,
|
||||
globalSettings.PushRelayBaseUri,
|
||||
@ -46,27 +41,10 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
|
||||
logger)
|
||||
{
|
||||
_deviceRepository = deviceRepository;
|
||||
_globalSettings = globalSettings;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_timeProvider = timeProvider;
|
||||
}
|
||||
|
||||
public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
{
|
||||
await PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds);
|
||||
}
|
||||
|
||||
public async Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
{
|
||||
await PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds);
|
||||
}
|
||||
|
||||
public async Task PushSyncCipherDeleteAsync(Cipher cipher)
|
||||
{
|
||||
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
|
||||
}
|
||||
|
||||
private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
|
||||
public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
|
||||
{
|
||||
if (cipher.OrganizationId.HasValue)
|
||||
{
|
||||
@ -87,306 +65,45 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
|
||||
RevisionDate = cipher.RevisionDate,
|
||||
};
|
||||
|
||||
await SendPayloadToUserAsync(cipher.UserId.Value, type, message, true);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PushSyncFolderCreateAsync(Folder folder)
|
||||
{
|
||||
await PushFolderAsync(folder, PushType.SyncFolderCreate);
|
||||
}
|
||||
|
||||
public async Task PushSyncFolderUpdateAsync(Folder folder)
|
||||
{
|
||||
await PushFolderAsync(folder, PushType.SyncFolderUpdate);
|
||||
}
|
||||
|
||||
public async Task PushSyncFolderDeleteAsync(Folder folder)
|
||||
{
|
||||
await PushFolderAsync(folder, PushType.SyncFolderDelete);
|
||||
}
|
||||
|
||||
private async Task PushFolderAsync(Folder folder, PushType type)
|
||||
{
|
||||
var message = new SyncFolderPushNotification
|
||||
{
|
||||
Id = folder.Id,
|
||||
UserId = folder.UserId,
|
||||
RevisionDate = folder.RevisionDate
|
||||
};
|
||||
|
||||
await SendPayloadToUserAsync(folder.UserId, type, message, true);
|
||||
}
|
||||
|
||||
public async Task PushSyncCiphersAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncCiphers);
|
||||
}
|
||||
|
||||
public async Task PushSyncVaultAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncVault);
|
||||
}
|
||||
|
||||
public async Task PushSyncOrganizationsAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncOrganizations);
|
||||
}
|
||||
|
||||
public async Task PushSyncOrgKeysAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncOrgKeys);
|
||||
}
|
||||
|
||||
public async Task PushSyncSettingsAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.SyncSettings);
|
||||
}
|
||||
|
||||
public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.LogOut, excludeCurrentContext);
|
||||
}
|
||||
|
||||
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
|
||||
{
|
||||
var message = new UserPushNotification { UserId = userId, Date = _timeProvider.GetUtcNow().UtcDateTime };
|
||||
|
||||
await SendPayloadToUserAsync(userId, type, message, excludeCurrentContext);
|
||||
}
|
||||
|
||||
public async Task PushSyncSendCreateAsync(Send send)
|
||||
{
|
||||
await PushSendAsync(send, PushType.SyncSendCreate);
|
||||
}
|
||||
|
||||
public async Task PushSyncSendUpdateAsync(Send send)
|
||||
{
|
||||
await PushSendAsync(send, PushType.SyncSendUpdate);
|
||||
}
|
||||
|
||||
public async Task PushSyncSendDeleteAsync(Send send)
|
||||
{
|
||||
await PushSendAsync(send, PushType.SyncSendDelete);
|
||||
}
|
||||
|
||||
private async Task PushSendAsync(Send send, PushType type)
|
||||
{
|
||||
if (send.UserId.HasValue)
|
||||
{
|
||||
var message = new SyncSendPushNotification
|
||||
await PushAsync(new PushNotification<SyncCipherPushNotification>
|
||||
{
|
||||
Id = send.Id,
|
||||
UserId = send.UserId.Value,
|
||||
RevisionDate = send.RevisionDate
|
||||
};
|
||||
|
||||
await SendPayloadToUserAsync(message.UserId, type, message, true);
|
||||
Type = type,
|
||||
Target = NotificationTarget.User,
|
||||
TargetId = cipher.UserId.Value,
|
||||
Payload = message,
|
||||
ExcludeCurrentContext = true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PushAuthRequestAsync(AuthRequest authRequest)
|
||||
public async Task PushAsync<T>(PushNotification<T> pushNotification)
|
||||
where T : class
|
||||
{
|
||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequest);
|
||||
}
|
||||
var deviceIdentifier = _httpContextAccessor.HttpContext
|
||||
?.RequestServices.GetService<ICurrentContext>()
|
||||
?.DeviceIdentifier;
|
||||
|
||||
public async Task PushAuthRequestResponseAsync(AuthRequest authRequest)
|
||||
{
|
||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
|
||||
}
|
||||
Guid? deviceId = null;
|
||||
|
||||
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
|
||||
{
|
||||
var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId };
|
||||
|
||||
await SendPayloadToUserAsync(authRequest.UserId, type, message, true);
|
||||
}
|
||||
|
||||
public async Task PushNotificationAsync(Notification notification)
|
||||
{
|
||||
var message = new NotificationPushNotification
|
||||
if (!string.IsNullOrEmpty(deviceIdentifier))
|
||||
{
|
||||
Id = notification.Id,
|
||||
Priority = notification.Priority,
|
||||
Global = notification.Global,
|
||||
ClientType = notification.ClientType,
|
||||
UserId = notification.UserId,
|
||||
OrganizationId = notification.OrganizationId,
|
||||
InstallationId = notification.Global ? _globalSettings.Installation.Id : null,
|
||||
TaskId = notification.TaskId,
|
||||
Title = notification.Title,
|
||||
Body = notification.Body,
|
||||
CreationDate = notification.CreationDate,
|
||||
RevisionDate = notification.RevisionDate
|
||||
var device = await _deviceRepository.GetByIdentifierAsync(deviceIdentifier);
|
||||
deviceId = device?.Id;
|
||||
}
|
||||
|
||||
var payload = new PushSendRequestModel<T>
|
||||
{
|
||||
Type = pushNotification.Type,
|
||||
UserId = pushNotification.GetTargetWhen(NotificationTarget.User),
|
||||
OrganizationId = pushNotification.GetTargetWhen(NotificationTarget.Organization),
|
||||
InstallationId = pushNotification.GetTargetWhen(NotificationTarget.Installation),
|
||||
Payload = pushNotification.Payload,
|
||||
Identifier = pushNotification.ExcludeCurrentContext ? deviceIdentifier : null,
|
||||
// We set the device id regardless of if they want to exclude the current context or not
|
||||
DeviceId = deviceId,
|
||||
ClientType = pushNotification.ClientType,
|
||||
};
|
||||
|
||||
if (notification.Global)
|
||||
{
|
||||
await SendPayloadToInstallationAsync(PushType.Notification, message, true, notification.ClientType);
|
||||
}
|
||||
else if (notification.UserId.HasValue)
|
||||
{
|
||||
await SendPayloadToUserAsync(notification.UserId.Value, PushType.Notification, message, true,
|
||||
notification.ClientType);
|
||||
}
|
||||
else if (notification.OrganizationId.HasValue)
|
||||
{
|
||||
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.Notification, message,
|
||||
true, notification.ClientType);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Invalid notification id {NotificationId} push notification", notification.Id);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
InstallationId = notification.Global ? _globalSettings.Installation.Id : null,
|
||||
TaskId = notification.TaskId,
|
||||
Title = notification.Title,
|
||||
Body = notification.Body,
|
||||
CreationDate = notification.CreationDate,
|
||||
RevisionDate = notification.RevisionDate,
|
||||
ReadDate = notificationStatus.ReadDate,
|
||||
DeletedDate = notificationStatus.DeletedDate
|
||||
};
|
||||
|
||||
if (notification.Global)
|
||||
{
|
||||
await SendPayloadToInstallationAsync(PushType.NotificationStatus, message, true, notification.ClientType);
|
||||
}
|
||||
else if (notification.UserId.HasValue)
|
||||
{
|
||||
await SendPayloadToUserAsync(notification.UserId.Value, PushType.NotificationStatus, message, true,
|
||||
notification.ClientType);
|
||||
}
|
||||
else if (notification.OrganizationId.HasValue)
|
||||
{
|
||||
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.NotificationStatus, message,
|
||||
true, notification.ClientType);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Invalid notification status id {NotificationId} push notification", notification.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PushSyncOrganizationStatusAsync(Organization organization)
|
||||
{
|
||||
var message = new OrganizationStatusPushNotification
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Enabled = organization.Enabled
|
||||
};
|
||||
|
||||
await SendPayloadToOrganizationAsync(organization.Id, PushType.SyncOrganizationStatusChanged, message, false);
|
||||
}
|
||||
|
||||
public async Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) =>
|
||||
await SendPayloadToOrganizationAsync(
|
||||
organization.Id,
|
||||
PushType.SyncOrganizationCollectionSettingChanged,
|
||||
new OrganizationCollectionManagementPushNotification
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation,
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion,
|
||||
LimitItemDeletion = organization.LimitItemDeletion
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
public async Task PushPendingSecurityTasksAsync(Guid userId)
|
||||
{
|
||||
await PushUserAsync(userId, PushType.PendingSecurityTasks);
|
||||
}
|
||||
|
||||
private async Task SendPayloadToInstallationAsync(PushType type, object payload, bool excludeCurrentContext,
|
||||
ClientType? clientType = null)
|
||||
{
|
||||
var request = new PushSendRequestModel
|
||||
{
|
||||
InstallationId = _globalSettings.Installation.Id.ToString(),
|
||||
Type = type,
|
||||
Payload = payload,
|
||||
ClientType = clientType
|
||||
};
|
||||
|
||||
await AddCurrentContextAsync(request, excludeCurrentContext);
|
||||
await SendAsync(HttpMethod.Post, "push/send", request);
|
||||
}
|
||||
|
||||
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 SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier,
|
||||
string? deviceId = null, ClientType? clientType = null) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
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();
|
||||
await SendAsync(HttpMethod.Post, "push/send", payload);
|
||||
}
|
||||
}
|
||||
|
@ -98,7 +98,10 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
|
||||
|
||||
return results
|
||||
.GroupBy(c => c.Id)
|
||||
.Select(g => g.OrderByDescending(og => og.Edit).ThenByDescending(og => og.ViewPassword).First())
|
||||
.Select(g =>
|
||||
g.OrderByDescending(og => og.Manage)
|
||||
.ThenByDescending(og => og.Edit)
|
||||
.ThenByDescending(og => og.ViewPassword).First())
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
@ -457,7 +457,7 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
IQueryable<CipherDetails> cipherDetailsView = withOrganizations ?
|
||||
var cipherDetailsView = withOrganizations ?
|
||||
new UserCipherDetailsQuery(userId).Run(dbContext) :
|
||||
new CipherDetailsQuery(userId).Run(dbContext);
|
||||
if (!withOrganizations)
|
||||
@ -485,8 +485,15 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
|
||||
Key = c.Key
|
||||
};
|
||||
}
|
||||
|
||||
var ciphers = await cipherDetailsView.ToListAsync();
|
||||
return ciphers;
|
||||
|
||||
return ciphers.GroupBy(c => c.Id)
|
||||
.Select(g => g.OrderByDescending(c => c.Manage)
|
||||
.ThenByDescending(c => c.Edit)
|
||||
.ThenByDescending(c => c.ViewPassword)
|
||||
.First())
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -288,7 +288,7 @@ public static class ServiceCollectionExtensions
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.Installation.Key))
|
||||
{
|
||||
services.AddKeyedSingleton<IPushNotificationService, RelayPushNotificationService>("implementation");
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPushEngine, RelayPushNotificationService>());
|
||||
services.AddSingleton<IPushRegistrationService, RelayPushRegistrationService>();
|
||||
}
|
||||
else
|
||||
@ -299,20 +299,20 @@ public static class ServiceCollectionExtensions
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications))
|
||||
{
|
||||
services.AddKeyedSingleton<IPushNotificationService, NotificationsApiPushNotificationService>("implementation");
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPushEngine, NotificationsApiPushNotificationService>());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<INotificationHubPool, NotificationHubPool>();
|
||||
services.AddSingleton<IPushRegistrationService, NotificationHubPushRegistrationService>();
|
||||
services.AddKeyedSingleton<IPushNotificationService, NotificationHubPushNotificationService>("implementation");
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPushEngine, NotificationHubPushNotificationService>());
|
||||
services.TryAddSingleton<IPushRelayer, NotificationHubPushNotificationService>();
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString))
|
||||
{
|
||||
services.AddKeyedSingleton("notifications",
|
||||
(_, _) => new QueueClient(globalSettings.Notifications.ConnectionString, "notifications"));
|
||||
services.AddKeyedSingleton<IPushNotificationService, AzureQueuePushNotificationService>(
|
||||
"implementation");
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPushEngine, AzureQueuePushNotificationService>());
|
||||
}
|
||||
}
|
||||
|
||||
@ -366,7 +366,6 @@ public static class ServiceCollectionExtensions
|
||||
{
|
||||
services.AddSingleton<IMailService, NoopMailService>();
|
||||
services.AddSingleton<IMailDeliveryService, NoopMailDeliveryService>();
|
||||
services.AddSingleton<IPushNotificationService, NoopPushNotificationService>();
|
||||
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
|
||||
services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>();
|
||||
services.AddSingleton<ILicensingService, NoopLicensingService>();
|
||||
@ -718,7 +717,8 @@ public static class ServiceCollectionExtensions
|
||||
retryQueueName: integrationRetryQueueName,
|
||||
maxRetries: maxRetries,
|
||||
rabbitMqService: provider.GetRequiredService<IRabbitMqService>(),
|
||||
logger: provider.GetRequiredService<ILogger<RabbitMqIntegrationListenerService>>()));
|
||||
logger: provider.GetRequiredService<ILogger<RabbitMqIntegrationListenerService>>(),
|
||||
timeProvider: provider.GetRequiredService<TimeProvider>()));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ public class ApiApplicationFactory : WebApplicationFactoryBase<Startup>
|
||||
_identityApplicationFactory.ManagesDatabase = false;
|
||||
}
|
||||
|
||||
public IdentityApplicationFactory Identity => _identityApplicationFactory;
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
base.ConfigureWebHost(builder);
|
||||
|
@ -0,0 +1,449 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Azure.Storage.Queues;
|
||||
using Bit.Api.IntegrationTest.Factories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.NotificationHub;
|
||||
using Bit.Core.Platform.Installations;
|
||||
using Bit.Core.Repositories;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using static Bit.Core.Settings.GlobalSettings;
|
||||
|
||||
namespace Bit.Api.IntegrationTest.Platform.Controllers;
|
||||
|
||||
public class PushControllerTests
|
||||
{
|
||||
private static readonly Guid _userId = Guid.NewGuid();
|
||||
private static readonly Guid _organizationId = Guid.NewGuid();
|
||||
private static readonly Guid _deviceId = Guid.NewGuid();
|
||||
|
||||
public static IEnumerable<object[]> SendData()
|
||||
{
|
||||
static object[] Typed<T>(PushSendRequestModel<T> pushSendRequestModel, string expectedHubTagExpression, bool expectHubCall = true)
|
||||
{
|
||||
return [pushSendRequestModel, expectedHubTagExpression, expectHubCall];
|
||||
}
|
||||
|
||||
static object[] UserTyped(PushType pushType)
|
||||
{
|
||||
return Typed(new PushSendRequestModel<UserPushNotification>
|
||||
{
|
||||
Type = pushType,
|
||||
UserId = _userId,
|
||||
DeviceId = _deviceId,
|
||||
Payload = new UserPushNotification
|
||||
{
|
||||
Date = DateTime.UtcNow,
|
||||
UserId = _userId,
|
||||
},
|
||||
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||
}
|
||||
|
||||
// User cipher
|
||||
yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>
|
||||
{
|
||||
Type = PushType.SyncCipherUpdate,
|
||||
UserId = _userId,
|
||||
DeviceId = _deviceId,
|
||||
Payload = new SyncCipherPushNotification
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = _userId,
|
||||
},
|
||||
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||
|
||||
// Organization cipher, an org cipher would not naturally be synced from our
|
||||
// code but it is technically possible to be submitted to the endpoint.
|
||||
yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>
|
||||
{
|
||||
Type = PushType.SyncCipherUpdate,
|
||||
OrganizationId = _organizationId,
|
||||
DeviceId = _deviceId,
|
||||
Payload = new SyncCipherPushNotification
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = _organizationId,
|
||||
},
|
||||
}, $"(template:payload && organizationId:%installation%_{_organizationId})");
|
||||
|
||||
yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>
|
||||
{
|
||||
Type = PushType.SyncCipherCreate,
|
||||
UserId = _userId,
|
||||
DeviceId = _deviceId,
|
||||
Payload = new SyncCipherPushNotification
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = _userId,
|
||||
},
|
||||
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||
|
||||
// Organization cipher, an org cipher would not naturally be synced from our
|
||||
// code but it is technically possible to be submitted to the endpoint.
|
||||
yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>
|
||||
{
|
||||
Type = PushType.SyncCipherCreate,
|
||||
OrganizationId = _organizationId,
|
||||
DeviceId = _deviceId,
|
||||
Payload = new SyncCipherPushNotification
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = _organizationId,
|
||||
},
|
||||
}, $"(template:payload && organizationId:%installation%_{_organizationId})");
|
||||
|
||||
yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>
|
||||
{
|
||||
Type = PushType.SyncCipherDelete,
|
||||
UserId = _userId,
|
||||
DeviceId = _deviceId,
|
||||
Payload = new SyncCipherPushNotification
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = _userId,
|
||||
},
|
||||
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||
|
||||
// Organization cipher, an org cipher would not naturally be synced from our
|
||||
// code but it is technically possible to be submitted to the endpoint.
|
||||
yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>
|
||||
{
|
||||
Type = PushType.SyncCipherDelete,
|
||||
OrganizationId = _organizationId,
|
||||
DeviceId = _deviceId,
|
||||
Payload = new SyncCipherPushNotification
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = _organizationId,
|
||||
},
|
||||
}, $"(template:payload && organizationId:%installation%_{_organizationId})");
|
||||
|
||||
yield return Typed(new PushSendRequestModel<SyncFolderPushNotification>
|
||||
{
|
||||
Type = PushType.SyncFolderDelete,
|
||||
UserId = _userId,
|
||||
DeviceId = _deviceId,
|
||||
Payload = new SyncFolderPushNotification
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = _userId,
|
||||
},
|
||||
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||
|
||||
yield return Typed(new PushSendRequestModel<SyncFolderPushNotification>
|
||||
{
|
||||
Type = PushType.SyncFolderCreate,
|
||||
UserId = _userId,
|
||||
DeviceId = _deviceId,
|
||||
Payload = new SyncFolderPushNotification
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = _userId,
|
||||
},
|
||||
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||
|
||||
yield return Typed(new PushSendRequestModel<SyncFolderPushNotification>
|
||||
{
|
||||
Type = PushType.SyncFolderCreate,
|
||||
UserId = _userId,
|
||||
DeviceId = _deviceId,
|
||||
Payload = new SyncFolderPushNotification
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = _userId,
|
||||
},
|
||||
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||
|
||||
yield return UserTyped(PushType.SyncCiphers);
|
||||
yield return UserTyped(PushType.SyncVault);
|
||||
yield return UserTyped(PushType.SyncOrganizations);
|
||||
yield return UserTyped(PushType.SyncOrgKeys);
|
||||
yield return UserTyped(PushType.SyncSettings);
|
||||
yield return UserTyped(PushType.LogOut);
|
||||
yield return UserTyped(PushType.PendingSecurityTasks);
|
||||
|
||||
yield return Typed(new PushSendRequestModel<AuthRequestPushNotification>
|
||||
{
|
||||
Type = PushType.AuthRequest,
|
||||
UserId = _userId,
|
||||
DeviceId = _deviceId,
|
||||
Payload = new AuthRequestPushNotification
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = _userId,
|
||||
},
|
||||
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||
|
||||
yield return Typed(new PushSendRequestModel<AuthRequestPushNotification>
|
||||
{
|
||||
Type = PushType.AuthRequestResponse,
|
||||
UserId = _userId,
|
||||
DeviceId = _deviceId,
|
||||
Payload = new AuthRequestPushNotification
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = _userId,
|
||||
},
|
||||
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||
|
||||
yield return Typed(new PushSendRequestModel<NotificationPushNotification>
|
||||
{
|
||||
Type = PushType.Notification,
|
||||
UserId = _userId,
|
||||
DeviceId = _deviceId,
|
||||
Payload = new NotificationPushNotification
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = _userId,
|
||||
},
|
||||
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||
|
||||
yield return Typed(new PushSendRequestModel<NotificationPushNotification>
|
||||
{
|
||||
Type = PushType.Notification,
|
||||
UserId = _userId,
|
||||
DeviceId = _deviceId,
|
||||
ClientType = ClientType.All,
|
||||
Payload = new NotificationPushNotification
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Global = true,
|
||||
},
|
||||
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||
|
||||
yield return Typed(new PushSendRequestModel<NotificationPushNotification>
|
||||
{
|
||||
Type = PushType.NotificationStatus,
|
||||
OrganizationId = _organizationId,
|
||||
DeviceId = _deviceId,
|
||||
Payload = new NotificationPushNotification
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = _userId,
|
||||
},
|
||||
}, $"(template:payload && organizationId:%installation%_{_organizationId})");
|
||||
|
||||
yield return Typed(new PushSendRequestModel<NotificationPushNotification>
|
||||
{
|
||||
Type = PushType.NotificationStatus,
|
||||
OrganizationId = _organizationId,
|
||||
DeviceId = _deviceId,
|
||||
Payload = new NotificationPushNotification
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = _userId,
|
||||
},
|
||||
}, $"(template:payload && organizationId:%installation%_{_organizationId})");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SendData))]
|
||||
public async Task Send_Works<T>(PushSendRequestModel<T> pushSendRequestModel, string expectedHubTagExpression, bool expectHubCall)
|
||||
{
|
||||
var (apiFactory, httpClient, installation, queueClient, notificationHubProxy) = await SetupTest();
|
||||
|
||||
// Act
|
||||
var pushSendResponse = await httpClient.PostAsJsonAsync("push/send", pushSendRequestModel);
|
||||
|
||||
// Assert
|
||||
pushSendResponse.EnsureSuccessStatusCode();
|
||||
|
||||
// Relayed notifications, the ones coming to this endpoint should
|
||||
// not make their way into our Azure Queue and instead should only be sent to Azure Notifications
|
||||
// hub.
|
||||
await queueClient
|
||||
.Received(0)
|
||||
.SendMessageAsync(Arg.Any<string>());
|
||||
|
||||
// Check that this notification was sent through hubs the expected number of times
|
||||
await notificationHubProxy
|
||||
.Received(expectHubCall ? 1 : 0)
|
||||
.SendTemplateNotificationAsync(
|
||||
Arg.Any<Dictionary<string, string>>(),
|
||||
Arg.Is(expectedHubTagExpression.Replace("%installation%", installation.Id.ToString()))
|
||||
);
|
||||
|
||||
// TODO: Expect on the dictionary more?
|
||||
|
||||
// Notifications being relayed from SH should have the device id
|
||||
// tracked so that we can later send the notification to that device.
|
||||
await apiFactory.GetService<IInstallationDeviceRepository>()
|
||||
.Received(1)
|
||||
.UpsertAsync(Arg.Is<InstallationDeviceEntity>(
|
||||
ide => ide.PartitionKey == installation.Id.ToString() && ide.RowKey == pushSendRequestModel.DeviceId.ToString()
|
||||
));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Send_InstallationNotification_NotAuthenticatedInstallation_Fails()
|
||||
{
|
||||
var (_, httpClient, _, _, _) = await SetupTest();
|
||||
|
||||
var response = await httpClient.PostAsJsonAsync("push/send", new PushSendRequestModel<object>
|
||||
{
|
||||
Type = PushType.NotificationStatus,
|
||||
InstallationId = Guid.NewGuid(),
|
||||
Payload = new { }
|
||||
});
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<JsonNode>();
|
||||
Assert.Equal(JsonValueKind.Object, body.GetValueKind());
|
||||
Assert.True(body.AsObject().TryGetPropertyValue("message", out var message));
|
||||
Assert.Equal(JsonValueKind.String, message.GetValueKind());
|
||||
Assert.Equal("InstallationId does not match current context.", message.GetValue<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Send_InstallationNotification_Works()
|
||||
{
|
||||
var (apiFactory, httpClient, installation, _, notificationHubProxy) = await SetupTest();
|
||||
|
||||
var deviceId = Guid.NewGuid();
|
||||
|
||||
var response = await httpClient.PostAsJsonAsync("push/send", new PushSendRequestModel<object>
|
||||
{
|
||||
Type = PushType.NotificationStatus,
|
||||
InstallationId = installation.Id,
|
||||
Payload = new { },
|
||||
DeviceId = deviceId,
|
||||
ClientType = ClientType.Web,
|
||||
});
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await notificationHubProxy
|
||||
.Received(1)
|
||||
.SendTemplateNotificationAsync(
|
||||
Arg.Any<Dictionary<string, string>>(),
|
||||
Arg.Is($"(template:payload && installationId:{installation.Id} && clientType:Web)")
|
||||
);
|
||||
|
||||
await apiFactory.GetService<IInstallationDeviceRepository>()
|
||||
.Received(1)
|
||||
.UpsertAsync(Arg.Is<InstallationDeviceEntity>(
|
||||
ide => ide.PartitionKey == installation.Id.ToString() && ide.RowKey == deviceId.ToString()
|
||||
));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Send_NoOrganizationNoInstallationNoUser_FailsModelValidation()
|
||||
{
|
||||
var (_, client, _, _, _) = await SetupTest();
|
||||
|
||||
var response = await client.PostAsJsonAsync("push/send", new PushSendRequestModel<object>
|
||||
{
|
||||
Type = PushType.AuthRequest,
|
||||
Payload = new { },
|
||||
});
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<JsonNode>();
|
||||
Assert.Equal(JsonValueKind.Object, body.GetValueKind());
|
||||
Assert.True(body.AsObject().TryGetPropertyValue("message", out var message));
|
||||
Assert.Equal(JsonValueKind.String, message.GetValueKind());
|
||||
Assert.Equal("The model state is invalid.", message.GetValue<string>());
|
||||
}
|
||||
|
||||
private static async Task<(ApiApplicationFactory Factory, HttpClient AuthedClient, Installation Installation, QueueClient MockedQueue, INotificationHubProxy MockedHub)> SetupTest()
|
||||
{
|
||||
// Arrange
|
||||
var apiFactory = new ApiApplicationFactory();
|
||||
|
||||
var queueClient = Substitute.For<QueueClient>();
|
||||
|
||||
// Substitute the underlying queue messages will go to.
|
||||
apiFactory.ConfigureServices(services =>
|
||||
{
|
||||
var queueClientService = services.FirstOrDefault(
|
||||
sd => sd.ServiceKey == (object)"notifications"
|
||||
&& sd.ServiceType == typeof(QueueClient)
|
||||
) ?? throw new InvalidOperationException("Expected service was not found.");
|
||||
|
||||
services.Remove(queueClientService);
|
||||
|
||||
services.AddKeyedSingleton("notifications", queueClient);
|
||||
});
|
||||
|
||||
var notificationHubProxy = Substitute.For<INotificationHubProxy>();
|
||||
|
||||
apiFactory.SubstituteService<INotificationHubPool>(s =>
|
||||
{
|
||||
s.AllClients
|
||||
.Returns(notificationHubProxy);
|
||||
});
|
||||
|
||||
apiFactory.SubstituteService<IInstallationDeviceRepository>(s => { });
|
||||
|
||||
// Setup as cloud with NotificationHub setup and Azure Queue
|
||||
apiFactory.UpdateConfiguration("GlobalSettings:Notifications:ConnectionString", "any_value");
|
||||
|
||||
// Configure hubs
|
||||
var index = 0;
|
||||
void AddHub(NotificationHubSettings notificationHubSettings)
|
||||
{
|
||||
apiFactory.UpdateConfiguration(
|
||||
$"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:ConnectionString",
|
||||
notificationHubSettings.ConnectionString
|
||||
);
|
||||
apiFactory.UpdateConfiguration(
|
||||
$"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:HubName",
|
||||
notificationHubSettings.HubName
|
||||
);
|
||||
apiFactory.UpdateConfiguration(
|
||||
$"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:RegistrationStartDate",
|
||||
notificationHubSettings.RegistrationStartDate?.ToString()
|
||||
);
|
||||
apiFactory.UpdateConfiguration(
|
||||
$"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:RegistrationEndDate",
|
||||
notificationHubSettings.RegistrationEndDate?.ToString()
|
||||
);
|
||||
index++;
|
||||
}
|
||||
|
||||
AddHub(new NotificationHubSettings
|
||||
{
|
||||
ConnectionString = "some_value",
|
||||
RegistrationStartDate = DateTime.UtcNow.AddDays(-2),
|
||||
});
|
||||
|
||||
var httpClient = apiFactory.CreateClient();
|
||||
|
||||
// Add installation into database
|
||||
var installationRepository = apiFactory.GetService<IInstallationRepository>();
|
||||
var installation = await installationRepository.CreateAsync(new Installation
|
||||
{
|
||||
Key = "my_test_key",
|
||||
Email = "test@example.com",
|
||||
Enabled = true,
|
||||
});
|
||||
|
||||
var identityClient = apiFactory.Identity.CreateDefaultClient();
|
||||
|
||||
var connectTokenResponse = await identityClient.PostAsync("connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
{ "grant_type", "client_credentials" },
|
||||
{ "scope", "api.push" },
|
||||
{ "client_id", $"installation.{installation.Id}" },
|
||||
{ "client_secret", installation.Key },
|
||||
}));
|
||||
|
||||
connectTokenResponse.EnsureSuccessStatusCode();
|
||||
|
||||
var connectTokenResponseModel = await connectTokenResponse.Content.ReadFromJsonAsync<JsonNode>();
|
||||
|
||||
// Setup authentication
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
|
||||
connectTokenResponseModel["token_type"].GetValue<string>(),
|
||||
connectTokenResponseModel["access_token"].GetValue<string>()
|
||||
);
|
||||
|
||||
return (apiFactory, httpClient, installation, queueClient, notificationHubProxy);
|
||||
}
|
||||
}
|
@ -18,210 +18,6 @@ namespace Bit.Api.Test.Platform.Push.Controllers;
|
||||
[SutProviderCustomize]
|
||||
public class PushControllerTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData(false, true)]
|
||||
[BitAutoData(false, false)]
|
||||
[BitAutoData(true, true)]
|
||||
public async Task SendAsync_InstallationIdNotSetOrSelfHosted_BadRequest(bool haveInstallationId, bool selfHosted,
|
||||
SutProvider<PushController> sutProvider, Guid installationId, Guid userId, Guid organizationId)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = selfHosted;
|
||||
if (haveInstallationId)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
|
||||
}
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.SendAsync(new PushSendRequestModel
|
||||
{
|
||||
Type = PushType.Notification,
|
||||
UserId = userId.ToString(),
|
||||
OrganizationId = organizationId.ToString(),
|
||||
InstallationId = installationId.ToString(),
|
||||
Payload = "test-payload"
|
||||
}));
|
||||
|
||||
Assert.Equal("Not correctly configured for push relays.", exception.Message);
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
||||
.SendPayloadToUserAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(), Arg.Any<string>(),
|
||||
Arg.Any<string?>(), Arg.Any<ClientType?>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
||||
.SendPayloadToOrganizationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
||||
.SendPayloadToInstallationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SendAsync_UserIdAndOrganizationIdAndInstallationIdEmpty_NoPushNotificationSent(
|
||||
SutProvider<PushController> sutProvider, Guid installationId)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;
|
||||
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
|
||||
|
||||
await sutProvider.Sut.SendAsync(new PushSendRequestModel
|
||||
{
|
||||
Type = PushType.Notification,
|
||||
UserId = null,
|
||||
OrganizationId = null,
|
||||
InstallationId = null,
|
||||
Payload = "test-payload"
|
||||
});
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
||||
.SendPayloadToUserAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(), Arg.Any<string>(),
|
||||
Arg.Any<string?>(), Arg.Any<ClientType?>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
||||
.SendPayloadToOrganizationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
||||
.SendPayloadToInstallationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[RepeatingPatternBitAutoData([false, true], [false, true], [false, true])]
|
||||
public async Task SendAsync_UserIdSet_SendPayloadToUserAsync(bool haveIdentifier, bool haveDeviceId,
|
||||
bool haveOrganizationId, SutProvider<PushController> sutProvider, Guid installationId, Guid userId,
|
||||
Guid identifier, Guid deviceId)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;
|
||||
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
|
||||
|
||||
var expectedUserId = $"{installationId}_{userId}";
|
||||
var expectedIdentifier = haveIdentifier ? $"{installationId}_{identifier}" : null;
|
||||
var expectedDeviceId = haveDeviceId ? $"{installationId}_{deviceId}" : null;
|
||||
|
||||
await sutProvider.Sut.SendAsync(new PushSendRequestModel
|
||||
{
|
||||
Type = PushType.Notification,
|
||||
UserId = userId.ToString(),
|
||||
OrganizationId = haveOrganizationId ? Guid.NewGuid().ToString() : null,
|
||||
InstallationId = null,
|
||||
Payload = "test-payload",
|
||||
DeviceId = haveDeviceId ? deviceId.ToString() : null,
|
||||
Identifier = haveIdentifier ? identifier.ToString() : null,
|
||||
ClientType = ClientType.All,
|
||||
});
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1)
|
||||
.SendPayloadToUserAsync(expectedUserId, PushType.Notification, "test-payload", expectedIdentifier,
|
||||
expectedDeviceId, ClientType.All);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
||||
.SendPayloadToOrganizationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
||||
.SendPayloadToInstallationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[RepeatingPatternBitAutoData([false, true], [false, true])]
|
||||
public async Task SendAsync_OrganizationIdSet_SendPayloadToOrganizationAsync(bool haveIdentifier, bool haveDeviceId,
|
||||
SutProvider<PushController> sutProvider, Guid installationId, Guid organizationId, Guid identifier,
|
||||
Guid deviceId)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;
|
||||
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
|
||||
|
||||
var expectedOrganizationId = $"{installationId}_{organizationId}";
|
||||
var expectedIdentifier = haveIdentifier ? $"{installationId}_{identifier}" : null;
|
||||
var expectedDeviceId = haveDeviceId ? $"{installationId}_{deviceId}" : null;
|
||||
|
||||
await sutProvider.Sut.SendAsync(new PushSendRequestModel
|
||||
{
|
||||
Type = PushType.Notification,
|
||||
UserId = null,
|
||||
OrganizationId = organizationId.ToString(),
|
||||
InstallationId = null,
|
||||
Payload = "test-payload",
|
||||
DeviceId = haveDeviceId ? deviceId.ToString() : null,
|
||||
Identifier = haveIdentifier ? identifier.ToString() : null,
|
||||
ClientType = ClientType.All,
|
||||
});
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1)
|
||||
.SendPayloadToOrganizationAsync(expectedOrganizationId, PushType.Notification, "test-payload",
|
||||
expectedIdentifier, expectedDeviceId, ClientType.All);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
||||
.SendPayloadToUserAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(), Arg.Any<string>(),
|
||||
Arg.Any<string?>(), Arg.Any<ClientType?>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
||||
.SendPayloadToInstallationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[RepeatingPatternBitAutoData([false, true], [false, true])]
|
||||
public async Task SendAsync_InstallationIdSet_SendPayloadToInstallationAsync(bool haveIdentifier, bool haveDeviceId,
|
||||
SutProvider<PushController> sutProvider, Guid installationId, Guid identifier, Guid deviceId)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;
|
||||
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
|
||||
|
||||
var expectedIdentifier = haveIdentifier ? $"{installationId}_{identifier}" : null;
|
||||
var expectedDeviceId = haveDeviceId ? $"{installationId}_{deviceId}" : null;
|
||||
|
||||
await sutProvider.Sut.SendAsync(new PushSendRequestModel
|
||||
{
|
||||
Type = PushType.Notification,
|
||||
UserId = null,
|
||||
OrganizationId = null,
|
||||
InstallationId = installationId.ToString(),
|
||||
Payload = "test-payload",
|
||||
DeviceId = haveDeviceId ? deviceId.ToString() : null,
|
||||
Identifier = haveIdentifier ? identifier.ToString() : null,
|
||||
ClientType = ClientType.All,
|
||||
});
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1)
|
||||
.SendPayloadToInstallationAsync(installationId.ToString(), PushType.Notification, "test-payload",
|
||||
expectedIdentifier, expectedDeviceId, ClientType.All);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
||||
.SendPayloadToOrganizationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
||||
.SendPayloadToUserAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(), Arg.Any<string>(),
|
||||
Arg.Any<string?>(), Arg.Any<ClientType?>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SendAsync_InstallationIdNotMatching_BadRequest(SutProvider<PushController> sutProvider,
|
||||
Guid installationId)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;
|
||||
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.SendAsync(new PushSendRequestModel
|
||||
{
|
||||
Type = PushType.Notification,
|
||||
UserId = null,
|
||||
OrganizationId = null,
|
||||
InstallationId = Guid.NewGuid().ToString(),
|
||||
Payload = "test-payload",
|
||||
DeviceId = null,
|
||||
Identifier = null,
|
||||
ClientType = ClientType.All,
|
||||
}));
|
||||
|
||||
Assert.Equal("InstallationId does not match current context.", exception.Message);
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
||||
.SendPayloadToInstallationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
||||
.SendPayloadToOrganizationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
||||
.SendPayloadToUserAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(), Arg.Any<string>(),
|
||||
Arg.Any<string?>(), Arg.Any<ClientType?>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(false, true)]
|
||||
[BitAutoData(false, false)]
|
||||
|
@ -52,7 +52,6 @@ public class AzureServiceBusIntegrationListenerServiceTests
|
||||
public async Task HandleMessageAsync_FailureNotRetryable_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
||||
message.RetryCount = 0;
|
||||
|
||||
var result = new IntegrationHandlerResult(false, message);
|
||||
@ -71,7 +70,6 @@ public class AzureServiceBusIntegrationListenerServiceTests
|
||||
public async Task HandleMessageAsync_FailureRetryableButTooManyRetries_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
||||
message.RetryCount = _maxRetries;
|
||||
var result = new IntegrationHandlerResult(false, message);
|
||||
result.Retryable = true;
|
||||
@ -90,12 +88,10 @@ public class AzureServiceBusIntegrationListenerServiceTests
|
||||
public async Task HandleMessageAsync_FailureRetryable_PublishesToRetryQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
||||
message.RetryCount = 0;
|
||||
|
||||
var result = new IntegrationHandlerResult(false, message);
|
||||
result.Retryable = true;
|
||||
result.DelayUntilDate = DateTime.UtcNow.AddMinutes(1);
|
||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||
|
||||
var expected = (IntegrationMessage<WebhookIntegrationConfiguration>)IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson())!;
|
||||
@ -110,7 +106,6 @@ public class AzureServiceBusIntegrationListenerServiceTests
|
||||
public async Task HandleMessageAsync_SuccessfulResult_Succeeds(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
||||
var result = new IntegrationHandlerResult(true, message);
|
||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||
|
||||
|
@ -4,6 +4,7 @@ using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using RabbitMQ.Client;
|
||||
using RabbitMQ.Client.Events;
|
||||
@ -18,19 +19,24 @@ public class RabbitMqIntegrationListenerServiceTests
|
||||
private const string _queueName = "test_queue";
|
||||
private const string _retryQueueName = "test_queue_retry";
|
||||
private const string _routingKey = "test_routing_key";
|
||||
private readonly DateTime _now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);
|
||||
private readonly IIntegrationHandler _handler = Substitute.For<IIntegrationHandler>();
|
||||
private readonly IRabbitMqService _rabbitMqService = Substitute.For<IRabbitMqService>();
|
||||
|
||||
private SutProvider<RabbitMqIntegrationListenerService> GetSutProvider()
|
||||
{
|
||||
return new SutProvider<RabbitMqIntegrationListenerService>()
|
||||
var sutProvider = new SutProvider<RabbitMqIntegrationListenerService>()
|
||||
.SetDependency(_handler)
|
||||
.SetDependency(_rabbitMqService)
|
||||
.SetDependency(_queueName, "queueName")
|
||||
.SetDependency(_retryQueueName, "retryQueueName")
|
||||
.SetDependency(_routingKey, "routingKey")
|
||||
.SetDependency(_maxRetries, "maxRetries")
|
||||
.WithFakeTimeProvider()
|
||||
.Create();
|
||||
sutProvider.GetDependency<FakeTimeProvider>().SetUtcNow(_now);
|
||||
|
||||
return sutProvider;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -55,7 +61,7 @@ public class RabbitMqIntegrationListenerServiceTests
|
||||
var cancellationToken = CancellationToken.None;
|
||||
await sutProvider.Sut.StartAsync(cancellationToken);
|
||||
|
||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
||||
message.DelayUntilDate = null;
|
||||
message.RetryCount = 0;
|
||||
var eventArgs = new BasicDeliverEventArgs(
|
||||
consumerTag: string.Empty,
|
||||
@ -94,7 +100,7 @@ public class RabbitMqIntegrationListenerServiceTests
|
||||
var cancellationToken = CancellationToken.None;
|
||||
await sutProvider.Sut.StartAsync(cancellationToken);
|
||||
|
||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
||||
message.DelayUntilDate = null;
|
||||
message.RetryCount = _maxRetries;
|
||||
var eventArgs = new BasicDeliverEventArgs(
|
||||
consumerTag: string.Empty,
|
||||
@ -132,7 +138,7 @@ public class RabbitMqIntegrationListenerServiceTests
|
||||
var cancellationToken = CancellationToken.None;
|
||||
await sutProvider.Sut.StartAsync(cancellationToken);
|
||||
|
||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
||||
message.DelayUntilDate = null;
|
||||
message.RetryCount = 0;
|
||||
var eventArgs = new BasicDeliverEventArgs(
|
||||
consumerTag: string.Empty,
|
||||
@ -145,7 +151,7 @@ public class RabbitMqIntegrationListenerServiceTests
|
||||
);
|
||||
var result = new IntegrationHandlerResult(false, message);
|
||||
result.Retryable = true;
|
||||
result.DelayUntilDate = DateTime.UtcNow.AddMinutes(1);
|
||||
result.DelayUntilDate = _now.AddMinutes(1);
|
||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||
|
||||
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
|
||||
@ -173,7 +179,7 @@ public class RabbitMqIntegrationListenerServiceTests
|
||||
var cancellationToken = CancellationToken.None;
|
||||
await sutProvider.Sut.StartAsync(cancellationToken);
|
||||
|
||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
||||
message.DelayUntilDate = null;
|
||||
var eventArgs = new BasicDeliverEventArgs(
|
||||
consumerTag: string.Empty,
|
||||
deliveryTag: 0,
|
||||
@ -205,7 +211,7 @@ public class RabbitMqIntegrationListenerServiceTests
|
||||
var cancellationToken = CancellationToken.None;
|
||||
await sutProvider.Sut.StartAsync(cancellationToken);
|
||||
|
||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(1);
|
||||
message.DelayUntilDate = _now.AddMinutes(1);
|
||||
var eventArgs = new BasicDeliverEventArgs(
|
||||
consumerTag: string.Empty,
|
||||
deliveryTag: 0,
|
||||
|
@ -5,6 +5,7 @@ using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Bit.Test.Common.MockedHttpClient;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
@ -33,6 +34,7 @@ public class WebhookIntegrationHandlerTests
|
||||
|
||||
return new SutProvider<WebhookIntegrationHandler>()
|
||||
.SetDependency(clientFactory)
|
||||
.WithFakeTimeProvider()
|
||||
.Create();
|
||||
}
|
||||
|
||||
@ -62,9 +64,13 @@ public class WebhookIntegrationHandlerTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_TooManyRequests_ReturnsFailureSetsNotBeforUtc(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
public async Task HandleAsync_TooManyRequests_ReturnsFailureSetsDelayUntilDate(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);
|
||||
var retryAfter = now.AddSeconds(60);
|
||||
|
||||
sutProvider.GetDependency<FakeTimeProvider>().SetUtcNow(now);
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
|
||||
|
||||
_handler.Fallback
|
||||
@ -78,19 +84,21 @@ public class WebhookIntegrationHandlerTests
|
||||
Assert.True(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.True(result.DelayUntilDate.HasValue);
|
||||
Assert.InRange(result.DelayUntilDate.Value, DateTime.UtcNow.AddSeconds(59), DateTime.UtcNow.AddSeconds(61));
|
||||
Assert.Equal(retryAfter, result.DelayUntilDate.Value);
|
||||
Assert.Equal("Too Many Requests", result.FailureReason);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_TooManyRequestsWithDate_ReturnsFailureSetsNotBeforUtc(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
public async Task HandleAsync_TooManyRequestsWithDate_ReturnsFailureSetsDelayUntilDate(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);
|
||||
var retryAfter = now.AddSeconds(60);
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
|
||||
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.TooManyRequests)
|
||||
.WithHeader("Retry-After", DateTime.UtcNow.AddSeconds(60).ToString("r")) // "r" is the round-trip format: RFC1123
|
||||
.WithHeader("Retry-After", retryAfter.ToString("r"))
|
||||
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
@ -99,7 +107,7 @@ public class WebhookIntegrationHandlerTests
|
||||
Assert.True(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.True(result.DelayUntilDate.HasValue);
|
||||
Assert.InRange(result.DelayUntilDate.Value, DateTime.UtcNow.AddSeconds(59), DateTime.UtcNow.AddSeconds(61));
|
||||
Assert.Equal(retryAfter, result.DelayUntilDate.Value);
|
||||
Assert.Equal("Too Many Requests", result.FailureReason);
|
||||
}
|
||||
|
||||
|
@ -11,16 +11,14 @@ namespace Bit.Core.Test.Models.Api.Request;
|
||||
|
||||
public class PushSendRequestModelTests
|
||||
{
|
||||
[Theory]
|
||||
[RepeatingPatternBitAutoData([null, "", " "], [null, "", " "], [null, "", " "])]
|
||||
public void Validate_UserIdOrganizationIdInstallationIdNullOrEmpty_Invalid(string? userId, string? organizationId,
|
||||
string? installationId)
|
||||
[Fact]
|
||||
public void Validate_UserIdOrganizationIdInstallationIdNull_Invalid()
|
||||
{
|
||||
var model = new PushSendRequestModel
|
||||
var model = new PushSendRequestModel<string>
|
||||
{
|
||||
UserId = userId,
|
||||
OrganizationId = organizationId,
|
||||
InstallationId = installationId,
|
||||
UserId = null,
|
||||
OrganizationId = null,
|
||||
InstallationId = null,
|
||||
Type = PushType.SyncCiphers,
|
||||
Payload = "test"
|
||||
};
|
||||
@ -32,16 +30,14 @@ public class PushSendRequestModelTests
|
||||
result => result.ErrorMessage == "UserId or OrganizationId or InstallationId is required.");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[RepeatingPatternBitAutoData([null, "", " "], [null, "", " "])]
|
||||
public void Validate_UserIdProvidedOrganizationIdInstallationIdNullOrEmpty_Valid(string? organizationId,
|
||||
string? installationId)
|
||||
[Fact]
|
||||
public void Validate_UserIdProvidedOrganizationIdInstallationIdNull_Valid()
|
||||
{
|
||||
var model = new PushSendRequestModel
|
||||
var model = new PushSendRequestModel<string>
|
||||
{
|
||||
UserId = Guid.NewGuid().ToString(),
|
||||
OrganizationId = organizationId,
|
||||
InstallationId = installationId,
|
||||
UserId = Guid.NewGuid(),
|
||||
OrganizationId = null,
|
||||
InstallationId = null,
|
||||
Type = PushType.SyncCiphers,
|
||||
Payload = "test"
|
||||
};
|
||||
@ -51,16 +47,14 @@ public class PushSendRequestModelTests
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[RepeatingPatternBitAutoData([null, "", " "], [null, "", " "])]
|
||||
public void Validate_OrganizationIdProvidedUserIdInstallationIdNullOrEmpty_Valid(string? userId,
|
||||
string? installationId)
|
||||
[Fact]
|
||||
public void Validate_OrganizationIdProvidedUserIdInstallationIdNull_Valid()
|
||||
{
|
||||
var model = new PushSendRequestModel
|
||||
var model = new PushSendRequestModel<string>
|
||||
{
|
||||
UserId = userId,
|
||||
OrganizationId = Guid.NewGuid().ToString(),
|
||||
InstallationId = installationId,
|
||||
UserId = null,
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
InstallationId = null,
|
||||
Type = PushType.SyncCiphers,
|
||||
Payload = "test"
|
||||
};
|
||||
@ -70,16 +64,14 @@ public class PushSendRequestModelTests
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[RepeatingPatternBitAutoData([null, "", " "], [null, "", " "])]
|
||||
public void Validate_InstallationIdProvidedUserIdOrganizationIdNullOrEmpty_Valid(string? userId,
|
||||
string? organizationId)
|
||||
[Fact]
|
||||
public void Validate_InstallationIdProvidedUserIdOrganizationIdNull_Valid()
|
||||
{
|
||||
var model = new PushSendRequestModel
|
||||
var model = new PushSendRequestModel<string>
|
||||
{
|
||||
UserId = userId,
|
||||
OrganizationId = organizationId,
|
||||
InstallationId = Guid.NewGuid().ToString(),
|
||||
UserId = null,
|
||||
OrganizationId = null,
|
||||
InstallationId = Guid.NewGuid(),
|
||||
Type = PushType.SyncCiphers,
|
||||
Payload = "test"
|
||||
};
|
||||
@ -94,10 +86,10 @@ public class PushSendRequestModelTests
|
||||
[BitAutoData("Type")]
|
||||
public void Validate_RequiredFieldNotProvided_Invalid(string requiredField)
|
||||
{
|
||||
var model = new PushSendRequestModel
|
||||
var model = new PushSendRequestModel<string>
|
||||
{
|
||||
UserId = Guid.NewGuid().ToString(),
|
||||
OrganizationId = Guid.NewGuid().ToString(),
|
||||
UserId = Guid.NewGuid(),
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
Type = PushType.SyncCiphers,
|
||||
Payload = "test"
|
||||
};
|
||||
@ -115,7 +107,7 @@ public class PushSendRequestModelTests
|
||||
|
||||
var serialized = JsonSerializer.Serialize(dictionary, JsonHelpers.IgnoreWritingNull);
|
||||
var jsonException =
|
||||
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<PushSendRequestModel>(serialized));
|
||||
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<PushSendRequestModel<string>>(serialized));
|
||||
Assert.Contains($"missing required properties, including the following: {requiredField}",
|
||||
jsonException.Message);
|
||||
}
|
||||
@ -123,15 +115,15 @@ public class PushSendRequestModelTests
|
||||
[Fact]
|
||||
public void Validate_AllFieldsPresent_Valid()
|
||||
{
|
||||
var model = new PushSendRequestModel
|
||||
var model = new PushSendRequestModel<string>
|
||||
{
|
||||
UserId = Guid.NewGuid().ToString(),
|
||||
OrganizationId = Guid.NewGuid().ToString(),
|
||||
UserId = Guid.NewGuid(),
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
Type = PushType.SyncCiphers,
|
||||
Payload = "test payload",
|
||||
Identifier = Guid.NewGuid().ToString(),
|
||||
ClientType = ClientType.All,
|
||||
DeviceId = Guid.NewGuid().ToString()
|
||||
DeviceId = Guid.NewGuid()
|
||||
};
|
||||
|
||||
var results = Validate(model);
|
||||
@ -139,7 +131,7 @@ public class PushSendRequestModelTests
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
private static List<ValidationResult> Validate(PushSendRequestModel model)
|
||||
private static List<ValidationResult> Validate<T>(PushSendRequestModel<T> model)
|
||||
{
|
||||
var results = new List<ValidationResult>();
|
||||
Validator.TryValidateObject(model, new ValidationContext(model), results, true);
|
||||
|
@ -5,12 +5,11 @@ using Bit.Core.Auth.Entities;
|
||||
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.NotificationCenter.Enums;
|
||||
using Bit.Core.NotificationHub;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Test.NotificationCenter.AutoFixture;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
@ -33,483 +32,6 @@ public class NotificationHubPushNotificationServiceTests
|
||||
private static readonly DateTime _now = DateTime.UtcNow;
|
||||
private static readonly Guid _installationId = Guid.Parse("da73177b-513f-4444-b582-595c890e1022");
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
[NotificationCustomize]
|
||||
public async Task PushNotificationAsync_GlobalInstallationIdDefault_NotSent(
|
||||
SutProvider<NotificationHubPushNotificationService> sutProvider, Notification notification)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = default;
|
||||
|
||||
await sutProvider.Sut.PushNotificationAsync(notification);
|
||||
|
||||
await sutProvider.GetDependency<INotificationHubPool>()
|
||||
.Received(0)
|
||||
.AllClients
|
||||
.Received(0)
|
||||
.SendTemplateNotificationAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<string>());
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
[NotificationCustomize]
|
||||
public async Task PushNotificationAsync_GlobalInstallationIdSetClientTypeAll_SentToInstallationId(
|
||||
SutProvider<NotificationHubPushNotificationService> sutProvider, Notification notification, Guid installationId)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = installationId;
|
||||
notification.ClientType = ClientType.All;
|
||||
var expectedNotification = ToNotificationPushNotification(notification, null, installationId);
|
||||
|
||||
await sutProvider.Sut.PushNotificationAsync(notification);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification,
|
||||
expectedNotification,
|
||||
$"(template:payload && installationId:{installationId})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(ClientType.Browser)]
|
||||
[BitAutoData(ClientType.Desktop)]
|
||||
[BitAutoData(ClientType.Web)]
|
||||
[BitAutoData(ClientType.Mobile)]
|
||||
[NotificationCustomize]
|
||||
public async Task PushNotificationAsync_GlobalInstallationIdSetClientTypeNotAll_SentToInstallationIdAndClientType(
|
||||
ClientType clientType, SutProvider<NotificationHubPushNotificationService> sutProvider,
|
||||
Notification notification, Guid installationId)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = installationId;
|
||||
notification.ClientType = clientType;
|
||||
var expectedNotification = ToNotificationPushNotification(notification, null, installationId);
|
||||
|
||||
await sutProvider.Sut.PushNotificationAsync(notification);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification,
|
||||
expectedNotification,
|
||||
$"(template:payload && installationId:{installationId} && clientType:{clientType})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(false)]
|
||||
[BitAutoData(true)]
|
||||
[NotificationCustomize(false)]
|
||||
public async Task PushNotificationAsync_UserIdProvidedClientTypeAll_SentToUser(
|
||||
bool organizationIdNull, SutProvider<NotificationHubPushNotificationService> sutProvider,
|
||||
Notification notification)
|
||||
{
|
||||
if (organizationIdNull)
|
||||
{
|
||||
notification.OrganizationId = null;
|
||||
}
|
||||
|
||||
notification.ClientType = ClientType.All;
|
||||
var expectedNotification = ToNotificationPushNotification(notification, null, null);
|
||||
|
||||
await sutProvider.Sut.PushNotificationAsync(notification);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification,
|
||||
expectedNotification,
|
||||
$"(template:payload_userId:{notification.UserId})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(ClientType.Browser)]
|
||||
[BitAutoData(ClientType.Desktop)]
|
||||
[BitAutoData(ClientType.Web)]
|
||||
[BitAutoData(ClientType.Mobile)]
|
||||
[NotificationCustomize(false)]
|
||||
public async Task PushNotificationAsync_UserIdProvidedOrganizationIdNullClientTypeNotAll_SentToUser(
|
||||
ClientType clientType, SutProvider<NotificationHubPushNotificationService> sutProvider,
|
||||
Notification notification)
|
||||
{
|
||||
notification.OrganizationId = null;
|
||||
notification.ClientType = clientType;
|
||||
var expectedNotification = ToNotificationPushNotification(notification, null, null);
|
||||
|
||||
await sutProvider.Sut.PushNotificationAsync(notification);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification,
|
||||
expectedNotification,
|
||||
$"(template:payload_userId:{notification.UserId} && clientType:{clientType})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(ClientType.Browser)]
|
||||
[BitAutoData(ClientType.Desktop)]
|
||||
[BitAutoData(ClientType.Web)]
|
||||
[BitAutoData(ClientType.Mobile)]
|
||||
[NotificationCustomize(false)]
|
||||
public async Task PushNotificationAsync_UserIdProvidedOrganizationIdProvidedClientTypeNotAll_SentToUser(
|
||||
ClientType clientType, SutProvider<NotificationHubPushNotificationService> sutProvider,
|
||||
Notification notification)
|
||||
{
|
||||
notification.ClientType = clientType;
|
||||
var expectedNotification = ToNotificationPushNotification(notification, null, null);
|
||||
|
||||
await sutProvider.Sut.PushNotificationAsync(notification);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification,
|
||||
expectedNotification,
|
||||
$"(template:payload_userId:{notification.UserId} && clientType:{clientType})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
[NotificationCustomize(false)]
|
||||
public async Task PushNotificationAsync_UserIdNullOrganizationIdProvidedClientTypeAll_SentToOrganization(
|
||||
SutProvider<NotificationHubPushNotificationService> sutProvider, Notification notification)
|
||||
{
|
||||
notification.UserId = null;
|
||||
notification.ClientType = ClientType.All;
|
||||
var expectedNotification = ToNotificationPushNotification(notification, null, null);
|
||||
|
||||
await sutProvider.Sut.PushNotificationAsync(notification);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification,
|
||||
expectedNotification,
|
||||
$"(template:payload && organizationId:{notification.OrganizationId})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(ClientType.Browser)]
|
||||
[BitAutoData(ClientType.Desktop)]
|
||||
[BitAutoData(ClientType.Web)]
|
||||
[BitAutoData(ClientType.Mobile)]
|
||||
[NotificationCustomize(false)]
|
||||
public async Task PushNotificationAsync_UserIdNullOrganizationIdProvidedClientTypeNotAll_SentToOrganization(
|
||||
ClientType clientType, SutProvider<NotificationHubPushNotificationService> sutProvider,
|
||||
Notification notification)
|
||||
{
|
||||
notification.UserId = null;
|
||||
notification.ClientType = clientType;
|
||||
var expectedNotification = ToNotificationPushNotification(notification, null, null);
|
||||
|
||||
await sutProvider.Sut.PushNotificationAsync(notification);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification,
|
||||
expectedNotification,
|
||||
$"(template:payload && organizationId:{notification.OrganizationId} && clientType:{clientType})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
[NotificationCustomize]
|
||||
public async Task PushNotificationStatusAsync_GlobalInstallationIdDefault_NotSent(
|
||||
SutProvider<NotificationHubPushNotificationService> sutProvider, Notification notification,
|
||||
NotificationStatus notificationStatus)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = default;
|
||||
|
||||
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
|
||||
|
||||
await sutProvider.GetDependency<INotificationHubPool>()
|
||||
.Received(0)
|
||||
.AllClients
|
||||
.Received(0)
|
||||
.SendTemplateNotificationAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<string>());
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
[NotificationCustomize]
|
||||
public async Task PushNotificationStatusAsync_GlobalInstallationIdSetClientTypeAll_SentToInstallationId(
|
||||
SutProvider<NotificationHubPushNotificationService> sutProvider,
|
||||
Notification notification, NotificationStatus notificationStatus, Guid installationId)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = installationId;
|
||||
notification.ClientType = ClientType.All;
|
||||
|
||||
var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, installationId);
|
||||
|
||||
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus,
|
||||
expectedNotification,
|
||||
$"(template:payload && installationId:{installationId})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(ClientType.Browser)]
|
||||
[BitAutoData(ClientType.Desktop)]
|
||||
[BitAutoData(ClientType.Web)]
|
||||
[BitAutoData(ClientType.Mobile)]
|
||||
[NotificationCustomize]
|
||||
public async Task
|
||||
PushNotificationStatusAsync_GlobalInstallationIdSetClientTypeNotAll_SentToInstallationIdAndClientType(
|
||||
ClientType clientType, SutProvider<NotificationHubPushNotificationService> sutProvider,
|
||||
Notification notification, NotificationStatus notificationStatus, Guid installationId)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = installationId;
|
||||
notification.ClientType = clientType;
|
||||
|
||||
var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, installationId);
|
||||
|
||||
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus,
|
||||
expectedNotification,
|
||||
$"(template:payload && installationId:{installationId} && clientType:{clientType})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(false)]
|
||||
[BitAutoData(true)]
|
||||
[NotificationCustomize(false)]
|
||||
public async Task PushNotificationStatusAsync_UserIdProvidedClientTypeAll_SentToUser(
|
||||
bool organizationIdNull, SutProvider<NotificationHubPushNotificationService> sutProvider,
|
||||
Notification notification, NotificationStatus notificationStatus)
|
||||
{
|
||||
if (organizationIdNull)
|
||||
{
|
||||
notification.OrganizationId = null;
|
||||
}
|
||||
|
||||
notification.ClientType = ClientType.All;
|
||||
var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null);
|
||||
|
||||
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus,
|
||||
expectedNotification,
|
||||
$"(template:payload_userId:{notification.UserId})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(ClientType.Browser)]
|
||||
[BitAutoData(ClientType.Desktop)]
|
||||
[BitAutoData(ClientType.Web)]
|
||||
[BitAutoData(ClientType.Mobile)]
|
||||
[NotificationCustomize(false)]
|
||||
public async Task PushNotificationStatusAsync_UserIdProvidedOrganizationIdNullClientTypeNotAll_SentToUser(
|
||||
ClientType clientType, SutProvider<NotificationHubPushNotificationService> sutProvider,
|
||||
Notification notification, NotificationStatus notificationStatus)
|
||||
{
|
||||
notification.OrganizationId = null;
|
||||
notification.ClientType = clientType;
|
||||
var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null);
|
||||
|
||||
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus,
|
||||
expectedNotification,
|
||||
$"(template:payload_userId:{notification.UserId} && clientType:{clientType})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(ClientType.Browser)]
|
||||
[BitAutoData(ClientType.Desktop)]
|
||||
[BitAutoData(ClientType.Web)]
|
||||
[BitAutoData(ClientType.Mobile)]
|
||||
[NotificationCustomize(false)]
|
||||
public async Task PushNotificationStatusAsync_UserIdProvidedOrganizationIdProvidedClientTypeNotAll_SentToUser(
|
||||
ClientType clientType, SutProvider<NotificationHubPushNotificationService> sutProvider,
|
||||
Notification notification, NotificationStatus notificationStatus)
|
||||
{
|
||||
notification.ClientType = clientType;
|
||||
var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null);
|
||||
|
||||
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus,
|
||||
expectedNotification,
|
||||
$"(template:payload_userId:{notification.UserId} && clientType:{clientType})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
[NotificationCustomize(false)]
|
||||
public async Task PushNotificationStatusAsync_UserIdNullOrganizationIdProvidedClientTypeAll_SentToOrganization(
|
||||
SutProvider<NotificationHubPushNotificationService> sutProvider, Notification notification,
|
||||
NotificationStatus notificationStatus)
|
||||
{
|
||||
notification.UserId = null;
|
||||
notification.ClientType = ClientType.All;
|
||||
var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null);
|
||||
|
||||
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus,
|
||||
expectedNotification,
|
||||
$"(template:payload && organizationId:{notification.OrganizationId})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(ClientType.Browser)]
|
||||
[BitAutoData(ClientType.Desktop)]
|
||||
[BitAutoData(ClientType.Web)]
|
||||
[BitAutoData(ClientType.Mobile)]
|
||||
[NotificationCustomize(false)]
|
||||
public async Task
|
||||
PushNotificationStatusAsync_UserIdNullOrganizationIdProvidedClientTypeNotAll_SentToOrganization(
|
||||
ClientType clientType, SutProvider<NotificationHubPushNotificationService> sutProvider,
|
||||
Notification notification, NotificationStatus notificationStatus)
|
||||
{
|
||||
notification.UserId = null;
|
||||
notification.ClientType = clientType;
|
||||
var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null);
|
||||
|
||||
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus,
|
||||
expectedNotification,
|
||||
$"(template:payload && organizationId:{notification.OrganizationId} && clientType:{clientType})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData([null])]
|
||||
[BitAutoData(ClientType.All)]
|
||||
public async Task SendPayloadToUserAsync_ClientTypeNullOrAll_SentToUser(ClientType? clientType,
|
||||
SutProvider<NotificationHubPushNotificationService> sutProvider, Guid userId, PushType pushType, string payload,
|
||||
string identifier)
|
||||
{
|
||||
await sutProvider.Sut.SendPayloadToUserAsync(userId.ToString(), pushType, payload, identifier, null,
|
||||
clientType);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload,
|
||||
$"(template:payload_userId:{userId} && !deviceIdentifier:{identifier})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(ClientType.Browser)]
|
||||
[BitAutoData(ClientType.Desktop)]
|
||||
[BitAutoData(ClientType.Mobile)]
|
||||
[BitAutoData(ClientType.Web)]
|
||||
public async Task SendPayloadToUserAsync_ClientTypeExplicit_SentToUserAndClientType(ClientType clientType,
|
||||
SutProvider<NotificationHubPushNotificationService> sutProvider, Guid userId, PushType pushType, string payload,
|
||||
string identifier)
|
||||
{
|
||||
await sutProvider.Sut.SendPayloadToUserAsync(userId.ToString(), pushType, payload, identifier, null,
|
||||
clientType);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload,
|
||||
$"(template:payload_userId:{userId} && !deviceIdentifier:{identifier} && clientType:{clientType})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData([null])]
|
||||
[BitAutoData(ClientType.All)]
|
||||
public async Task SendPayloadToOrganizationAsync_ClientTypeNullOrAll_SentToOrganization(ClientType? clientType,
|
||||
SutProvider<NotificationHubPushNotificationService> sutProvider, Guid organizationId, PushType pushType,
|
||||
string payload, string identifier)
|
||||
{
|
||||
await sutProvider.Sut.SendPayloadToOrganizationAsync(organizationId.ToString(), pushType, payload, identifier,
|
||||
null, clientType);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload,
|
||||
$"(template:payload && organizationId:{organizationId} && !deviceIdentifier:{identifier})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(ClientType.Browser)]
|
||||
[BitAutoData(ClientType.Desktop)]
|
||||
[BitAutoData(ClientType.Mobile)]
|
||||
[BitAutoData(ClientType.Web)]
|
||||
public async Task SendPayloadToOrganizationAsync_ClientTypeExplicit_SentToOrganizationAndClientType(
|
||||
ClientType clientType, SutProvider<NotificationHubPushNotificationService> sutProvider, Guid organizationId,
|
||||
PushType pushType, string payload, string identifier)
|
||||
{
|
||||
await sutProvider.Sut.SendPayloadToOrganizationAsync(organizationId.ToString(), pushType, payload, identifier,
|
||||
null, clientType);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload,
|
||||
$"(template:payload && organizationId:{organizationId} && !deviceIdentifier:{identifier} && clientType:{clientType})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData([null])]
|
||||
[BitAutoData(ClientType.All)]
|
||||
public async Task SendPayloadToInstallationAsync_ClientTypeNullOrAll_SentToInstallation(ClientType? clientType,
|
||||
SutProvider<NotificationHubPushNotificationService> sutProvider, Guid installationId, PushType pushType,
|
||||
string payload, string identifier)
|
||||
{
|
||||
await sutProvider.Sut.SendPayloadToInstallationAsync(installationId.ToString(), pushType, payload, identifier,
|
||||
null, clientType);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload,
|
||||
$"(template:payload && installationId:{installationId} && !deviceIdentifier:{identifier})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(ClientType.Browser)]
|
||||
[BitAutoData(ClientType.Desktop)]
|
||||
[BitAutoData(ClientType.Mobile)]
|
||||
[BitAutoData(ClientType.Web)]
|
||||
public async Task SendPayloadToInstallationAsync_ClientTypeExplicit_SentToInstallationAndClientType(
|
||||
ClientType clientType, SutProvider<NotificationHubPushNotificationService> sutProvider, Guid installationId,
|
||||
PushType pushType, string payload, string identifier)
|
||||
{
|
||||
await sutProvider.Sut.SendPayloadToInstallationAsync(installationId.ToString(), pushType, payload, identifier,
|
||||
null, clientType);
|
||||
|
||||
await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload,
|
||||
$"(template:payload && installationId:{installationId} && !deviceIdentifier:{identifier} && clientType:{clientType})");
|
||||
await sutProvider.GetDependency<IInstallationDeviceRepository>()
|
||||
.Received(0)
|
||||
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PushSyncCipherCreateAsync_SendExpectedData()
|
||||
{
|
||||
@ -1066,7 +588,7 @@ public class NotificationHubPushNotificationServiceTests
|
||||
);
|
||||
}
|
||||
|
||||
private async Task VerifyNotificationAsync(Func<NotificationHubPushNotificationService, Task> test,
|
||||
private async Task VerifyNotificationAsync(Func<IPushNotificationService, Task> test,
|
||||
PushType type, JsonNode expectedPayload, string tag)
|
||||
{
|
||||
var installationDeviceRepository = Substitute.For<IInstallationDeviceRepository>();
|
||||
@ -1104,12 +626,11 @@ public class NotificationHubPushNotificationServiceTests
|
||||
notificationHubPool,
|
||||
httpContextAccessor,
|
||||
NullLogger<NotificationHubPushNotificationService>.Instance,
|
||||
globalSettings,
|
||||
fakeTimeProvider
|
||||
globalSettings
|
||||
);
|
||||
|
||||
// Act
|
||||
await test(sut);
|
||||
await test(new EngineWrapper(sut, fakeTimeProvider, _installationId));
|
||||
|
||||
// Assert
|
||||
var calls = notificationHubProxy.ReceivedCalls();
|
||||
|
@ -9,14 +9,11 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.NotificationCenter.Entities;
|
||||
using Bit.Core.NotificationCenter.Enums;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Platform.Push.Internal;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Test.AutoFixture;
|
||||
using Bit.Core.Test.AutoFixture.CurrentContextFixtures;
|
||||
using Bit.Core.Test.NotificationCenter.AutoFixture;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -42,96 +39,6 @@ public class AzureQueuePushNotificationServiceTests
|
||||
_fakeTimeProvider.SetUtcNow(DateTime.UtcNow);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
[NotificationCustomize]
|
||||
[CurrentContextCustomize]
|
||||
public async Task PushNotificationAsync_NotificationGlobal_Sent(
|
||||
SutProvider<AzureQueuePushNotificationService> sutProvider, Notification notification, Guid deviceIdentifier,
|
||||
ICurrentContext currentContext, Guid installationId)
|
||||
{
|
||||
currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString());
|
||||
sutProvider.GetDependency<IHttpContextAccessor>().HttpContext!.RequestServices
|
||||
.GetService(Arg.Any<Type>()).Returns(currentContext);
|
||||
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = installationId;
|
||||
|
||||
await sutProvider.Sut.PushNotificationAsync(notification);
|
||||
|
||||
await sutProvider.GetDependency<QueueClient>().Received(1)
|
||||
.SendMessageAsync(Arg.Is<string>(message =>
|
||||
MatchMessage(PushType.Notification, message,
|
||||
new NotificationPushNotificationEquals(notification, null, installationId),
|
||||
deviceIdentifier.ToString())));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
[NotificationCustomize(false)]
|
||||
[CurrentContextCustomize]
|
||||
public async Task PushNotificationAsync_NotificationNotGlobal_Sent(
|
||||
SutProvider<AzureQueuePushNotificationService> sutProvider, Notification notification, Guid deviceIdentifier,
|
||||
ICurrentContext currentContext, Guid installationId)
|
||||
{
|
||||
currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString());
|
||||
sutProvider.GetDependency<IHttpContextAccessor>().HttpContext!.RequestServices
|
||||
.GetService(Arg.Any<Type>()).Returns(currentContext);
|
||||
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = installationId;
|
||||
|
||||
await sutProvider.Sut.PushNotificationAsync(notification);
|
||||
|
||||
await sutProvider.GetDependency<QueueClient>().Received(1)
|
||||
.SendMessageAsync(Arg.Is<string>(message =>
|
||||
MatchMessage(PushType.Notification, message,
|
||||
new NotificationPushNotificationEquals(notification, null, null),
|
||||
deviceIdentifier.ToString())));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
[NotificationCustomize]
|
||||
[NotificationStatusCustomize]
|
||||
[CurrentContextCustomize]
|
||||
public async Task PushNotificationStatusAsync_NotificationGlobal_Sent(
|
||||
SutProvider<AzureQueuePushNotificationService> sutProvider, Notification notification, Guid deviceIdentifier,
|
||||
ICurrentContext currentContext, NotificationStatus notificationStatus, Guid installationId)
|
||||
{
|
||||
currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString());
|
||||
sutProvider.GetDependency<IHttpContextAccessor>().HttpContext!.RequestServices
|
||||
.GetService(Arg.Any<Type>()).Returns(currentContext);
|
||||
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = installationId;
|
||||
|
||||
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
|
||||
|
||||
await sutProvider.GetDependency<QueueClient>().Received(1)
|
||||
.SendMessageAsync(Arg.Is<string>(message =>
|
||||
MatchMessage(PushType.NotificationStatus, message,
|
||||
new NotificationPushNotificationEquals(notification, notificationStatus, installationId),
|
||||
deviceIdentifier.ToString())));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
[NotificationCustomize(false)]
|
||||
[NotificationStatusCustomize]
|
||||
[CurrentContextCustomize]
|
||||
public async Task PushNotificationStatusAsync_NotificationNotGlobal_Sent(
|
||||
SutProvider<AzureQueuePushNotificationService> sutProvider, Notification notification, Guid deviceIdentifier,
|
||||
ICurrentContext currentContext, NotificationStatus notificationStatus, Guid installationId)
|
||||
{
|
||||
currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString());
|
||||
sutProvider.GetDependency<IHttpContextAccessor>().HttpContext!.RequestServices
|
||||
.GetService(Arg.Any<Type>()).Returns(currentContext);
|
||||
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = installationId;
|
||||
|
||||
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
|
||||
|
||||
await sutProvider.GetDependency<QueueClient>().Received(1)
|
||||
.SendMessageAsync(Arg.Is<string>(message =>
|
||||
MatchMessage(PushType.NotificationStatus, message,
|
||||
new NotificationPushNotificationEquals(notification, notificationStatus, null),
|
||||
deviceIdentifier.ToString())));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("6a5bbe1b-cf16-49a6-965f-5c2eac56a531", null)]
|
||||
[InlineData(null, "b9a3fcb4-2447-45c1-aad2-24de43c88c44")]
|
||||
@ -844,7 +751,7 @@ public class AzureQueuePushNotificationServiceTests
|
||||
// );
|
||||
// }
|
||||
|
||||
private async Task VerifyNotificationAsync(Func<AzureQueuePushNotificationService, Task> test, JsonNode expectedMessage)
|
||||
private async Task VerifyNotificationAsync(Func<IPushNotificationService, Task> test, JsonNode expectedMessage)
|
||||
{
|
||||
var queueClient = Substitute.For<QueueClient>();
|
||||
|
||||
@ -872,7 +779,7 @@ public class AzureQueuePushNotificationServiceTests
|
||||
_fakeTimeProvider
|
||||
);
|
||||
|
||||
await test(sut);
|
||||
await test(new EngineWrapper(sut, _fakeTimeProvider, _globalSettings.Installation.Id));
|
||||
|
||||
// Hoist equality checker outside the expression so that we
|
||||
// can more easily place a breakpoint
|
||||
|
@ -1,98 +1,8 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.NotificationCenter.Entities;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Platform.Push.Internal;
|
||||
using Bit.Core.Test.NotificationCenter.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Platform.Push.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class MultiServicePushNotificationServiceTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
[NotificationCustomize]
|
||||
public async Task PushNotificationAsync_Notification_Sent(
|
||||
SutProvider<MultiServicePushNotificationService> sutProvider, Notification notification)
|
||||
{
|
||||
await sutProvider.Sut.PushNotificationAsync(notification);
|
||||
|
||||
await sutProvider.GetDependency<IEnumerable<IPushNotificationService>>()
|
||||
.First()
|
||||
.Received(1)
|
||||
.PushNotificationAsync(notification);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
[NotificationCustomize]
|
||||
[NotificationStatusCustomize]
|
||||
public async Task PushNotificationStatusAsync_Notification_Sent(
|
||||
SutProvider<MultiServicePushNotificationService> sutProvider, Notification notification,
|
||||
NotificationStatus notificationStatus)
|
||||
{
|
||||
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
|
||||
|
||||
await sutProvider.GetDependency<IEnumerable<IPushNotificationService>>()
|
||||
.First()
|
||||
.Received(1)
|
||||
.PushNotificationStatusAsync(notification, notificationStatus);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData([null, null])]
|
||||
[BitAutoData(ClientType.All, null)]
|
||||
[BitAutoData([null, "test device id"])]
|
||||
[BitAutoData(ClientType.All, "test device id")]
|
||||
public async Task SendPayloadToUserAsync_Message_Sent(ClientType? clientType, string? deviceId, string userId,
|
||||
PushType type, object payload, string identifier, SutProvider<MultiServicePushNotificationService> sutProvider)
|
||||
{
|
||||
await sutProvider.Sut.SendPayloadToUserAsync(userId, type, payload, identifier, deviceId, clientType);
|
||||
|
||||
await sutProvider.GetDependency<IEnumerable<IPushNotificationService>>()
|
||||
.First()
|
||||
.Received(1)
|
||||
.SendPayloadToUserAsync(userId, type, payload, identifier, deviceId, clientType);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData([null, null])]
|
||||
[BitAutoData(ClientType.All, null)]
|
||||
[BitAutoData([null, "test device id"])]
|
||||
[BitAutoData(ClientType.All, "test device id")]
|
||||
public async Task SendPayloadToOrganizationAsync_Message_Sent(ClientType? clientType, string? deviceId,
|
||||
string organizationId, PushType type, object payload, string identifier,
|
||||
SutProvider<MultiServicePushNotificationService> sutProvider)
|
||||
{
|
||||
await sutProvider.Sut.SendPayloadToOrganizationAsync(organizationId, type, payload, identifier, deviceId,
|
||||
clientType);
|
||||
|
||||
await sutProvider.GetDependency<IEnumerable<IPushNotificationService>>()
|
||||
.First()
|
||||
.Received(1)
|
||||
.SendPayloadToOrganizationAsync(organizationId, type, payload, identifier, deviceId, clientType);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData([null, null])]
|
||||
[BitAutoData(ClientType.All, null)]
|
||||
[BitAutoData([null, "test device id"])]
|
||||
[BitAutoData(ClientType.All, "test device id")]
|
||||
public async Task SendPayloadToInstallationAsync_Message_Sent(ClientType? clientType, string? deviceId,
|
||||
string installationId, PushType type, object payload, string identifier,
|
||||
SutProvider<MultiServicePushNotificationService> sutProvider)
|
||||
{
|
||||
await sutProvider.Sut.SendPayloadToInstallationAsync(installationId, type, payload, identifier, deviceId,
|
||||
clientType);
|
||||
|
||||
await sutProvider.GetDependency<IEnumerable<IPushNotificationService>>()
|
||||
.First()
|
||||
.Received(1)
|
||||
.SendPayloadToInstallationAsync(installationId, type, payload, identifier, deviceId, clientType);
|
||||
}
|
||||
// TODO: Can add a couple tests here
|
||||
}
|
||||
|
@ -19,14 +19,13 @@ public class NotificationsApiPushNotificationServiceTests : PushTestBase
|
||||
|
||||
protected override string ExpectedClientUrl() => "https://localhost:7777/send";
|
||||
|
||||
protected override IPushNotificationService CreateService()
|
||||
protected override IPushEngine CreateService()
|
||||
{
|
||||
return new NotificationsApiPushNotificationService(
|
||||
HttpClientFactory,
|
||||
GlobalSettings,
|
||||
HttpContextAccessor,
|
||||
NullLogger<NotificationsApiPushNotificationService>.Instance,
|
||||
FakeTimeProvider
|
||||
NullLogger<NotificationsApiPushNotificationService>.Instance
|
||||
);
|
||||
}
|
||||
|
||||
@ -221,7 +220,7 @@ public class NotificationsApiPushNotificationServiceTests : PushTestBase
|
||||
["UserId"] = send.UserId,
|
||||
["RevisionDate"] = send.RevisionDate,
|
||||
},
|
||||
["ContextId"] = null,
|
||||
["ContextId"] = DeviceIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
@ -236,7 +235,7 @@ public class NotificationsApiPushNotificationServiceTests : PushTestBase
|
||||
["UserId"] = send.UserId,
|
||||
["RevisionDate"] = send.RevisionDate,
|
||||
},
|
||||
["ContextId"] = null,
|
||||
["ContextId"] = DeviceIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
@ -251,7 +250,7 @@ public class NotificationsApiPushNotificationServiceTests : PushTestBase
|
||||
["UserId"] = send.UserId,
|
||||
["RevisionDate"] = send.RevisionDate,
|
||||
},
|
||||
["ContextId"] = null,
|
||||
["ContextId"] = DeviceIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -15,11 +15,28 @@ using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using RichardSzalay.MockHttp;
|
||||
using Xunit;
|
||||
|
||||
public class EngineWrapper(IPushEngine pushEngine, FakeTimeProvider fakeTimeProvider, Guid installationId) : IPushNotificationService
|
||||
{
|
||||
public Guid InstallationId { get; } = installationId;
|
||||
|
||||
public TimeProvider TimeProvider { get; } = fakeTimeProvider;
|
||||
|
||||
public ILogger Logger => NullLogger<EngineWrapper>.Instance;
|
||||
|
||||
public Task PushAsync<T>(PushNotification<T> pushNotification) where T : class
|
||||
=> pushEngine.PushAsync(pushNotification);
|
||||
|
||||
public Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable<Guid>? collectionIds)
|
||||
=> pushEngine.PushCipherAsync(cipher, pushType, collectionIds);
|
||||
}
|
||||
|
||||
public abstract class PushTestBase
|
||||
{
|
||||
protected static readonly string DeviceIdentifier = "test_device_identifier";
|
||||
@ -51,7 +68,7 @@ public abstract class PushTestBase
|
||||
FakeTimeProvider.SetUtcNow(DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
protected abstract IPushNotificationService CreateService();
|
||||
protected abstract IPushEngine CreateService();
|
||||
|
||||
protected abstract string ExpectedClientUrl();
|
||||
|
||||
@ -480,7 +497,7 @@ public abstract class PushTestBase
|
||||
})
|
||||
.Respond(HttpStatusCode.OK);
|
||||
|
||||
await test(CreateService());
|
||||
await test(new EngineWrapper(CreateService(), FakeTimeProvider, GlobalSettings.Installation.Id));
|
||||
|
||||
Assert.NotNull(actualNode);
|
||||
|
||||
|
@ -4,8 +4,8 @@ using System.Text.Json.Nodes;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.NotificationCenter.Entities;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Platform.Push.Internal;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
@ -14,7 +14,6 @@ using Bit.Core.Vault.Entities;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Platform.Push.Services;
|
||||
|
||||
@ -38,47 +37,19 @@ public class RelayPushNotificationServiceTests : PushTestBase
|
||||
GlobalSettings.Installation.IdentityUri = "https://localhost:8888";
|
||||
}
|
||||
|
||||
protected override RelayPushNotificationService CreateService()
|
||||
protected override IPushEngine CreateService()
|
||||
{
|
||||
return new RelayPushNotificationService(
|
||||
HttpClientFactory,
|
||||
_deviceRepository,
|
||||
GlobalSettings,
|
||||
HttpContextAccessor,
|
||||
NullLogger<RelayPushNotificationService>.Instance,
|
||||
FakeTimeProvider
|
||||
NullLogger<RelayPushNotificationService>.Instance
|
||||
);
|
||||
}
|
||||
|
||||
protected override string ExpectedClientUrl() => "https://localhost:7777/push/send";
|
||||
|
||||
[Fact]
|
||||
public async Task SendPayloadToInstallationAsync_ThrowsNotImplementedException()
|
||||
{
|
||||
var sut = CreateService();
|
||||
await Assert.ThrowsAsync<NotImplementedException>(
|
||||
async () => await sut.SendPayloadToInstallationAsync("installation_id", PushType.AuthRequest, new { }, null)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendPayloadToUserAsync_ThrowsNotImplementedException()
|
||||
{
|
||||
var sut = CreateService();
|
||||
await Assert.ThrowsAsync<NotImplementedException>(
|
||||
async () => await sut.SendPayloadToUserAsync("user_id", PushType.AuthRequest, new { }, null)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendPayloadToOrganizationAsync_ThrowsNotImplementedException()
|
||||
{
|
||||
var sut = CreateService();
|
||||
await Assert.ThrowsAsync<NotImplementedException>(
|
||||
async () => await sut.SendPayloadToOrganizationAsync("organization_id", PushType.AuthRequest, new { }, null)
|
||||
);
|
||||
}
|
||||
|
||||
protected override JsonNode GetPushSyncCipherCreatePayload(Cipher cipher, Guid collectionIds)
|
||||
{
|
||||
return new JsonObject
|
||||
|
@ -571,6 +571,65 @@ public class CipherRepositoryTests
|
||||
Assert.True(personalDetails.Manage, "Personal ciphers should always have Manage permission");
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task GetManyByUserIdAsync_WhenOneCipherIsAssignedToTwoCollectionsWithDifferentPermissions_MostPrivilegedAccessIsReturnedOnTheCipher(
|
||||
ICipherRepository cipherRepository,
|
||||
IUserRepository userRepository,
|
||||
ICollectionCipherRepository collectionCipherRepository,
|
||||
ICollectionRepository collectionRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository)
|
||||
{
|
||||
//Arrange
|
||||
var (user, organization, orgUser) = await CreateTestUserAndOrganization(userRepository, organizationRepository, organizationUserRepository);
|
||||
|
||||
var cipher = await cipherRepository.CreateAsync(new Cipher
|
||||
{
|
||||
Type = CipherType.Login,
|
||||
OrganizationId = organization.Id,
|
||||
Data = ""
|
||||
});
|
||||
|
||||
var managedPermissionsCollection = await collectionRepository.CreateAsync(new Collection
|
||||
{
|
||||
Name = "Managed",
|
||||
OrganizationId = organization.Id
|
||||
});
|
||||
|
||||
var unmanagedPermissionsCollection = await collectionRepository.CreateAsync(new Collection
|
||||
{
|
||||
Name = "Unmanaged",
|
||||
OrganizationId = organization.Id
|
||||
});
|
||||
await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipher.Id, organization.Id,
|
||||
[managedPermissionsCollection.Id, unmanagedPermissionsCollection.Id]);
|
||||
|
||||
await collectionRepository.UpdateUsersAsync(managedPermissionsCollection.Id, new List<CollectionAccessSelection>
|
||||
{
|
||||
new() { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }
|
||||
});
|
||||
|
||||
await collectionRepository.UpdateUsersAsync(unmanagedPermissionsCollection.Id, new List<CollectionAccessSelection>
|
||||
{
|
||||
new() { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = false }
|
||||
});
|
||||
|
||||
// Act
|
||||
var ciphers = await cipherRepository.GetManyByUserIdAsync(user.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Single(ciphers);
|
||||
var deletableCipher = ciphers.SingleOrDefault(x => x.Id == cipher.Id);
|
||||
Assert.NotNull(deletableCipher);
|
||||
Assert.True(deletableCipher.Manage);
|
||||
|
||||
// Annul
|
||||
await cipherRepository.DeleteAsync(cipher);
|
||||
await organizationUserRepository.DeleteAsync(orgUser);
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
await userRepository.DeleteAsync(user);
|
||||
}
|
||||
|
||||
private async Task<(User user, Organization org, OrganizationUser orgUser)> CreateTestUserAndOrganization(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
|
@ -26,7 +26,7 @@ AS
|
||||
[dbo].[OrganizationReport];
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[OrganizationReport_Create]
|
||||
CREATE OR ALTER PROCEDURE [dbo].[OrganizationReport_Create]
|
||||
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@Date DATETIME2(7),
|
||||
|
Loading…
x
Reference in New Issue
Block a user