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; using Microsoft.AspNetCore.Mvc; namespace Bit.Api.Platform.Push; /// /// Routes for push relay: functionality that facilitates communication /// between self hosted organizations and Bitwarden cloud. /// [Route("push")] [Authorize("Push")] [SelfHosted(NotSelfHostedOnly = true)] public class PushController : Controller { private readonly IPushRegistrationService _pushRegistrationService; private readonly IPushRelayer _pushRelayer; private readonly IWebHostEnvironment _environment; private readonly ICurrentContext _currentContext; private readonly IGlobalSettings _globalSettings; public PushController( IPushRegistrationService pushRegistrationService, IPushRelayer pushRelayer, IWebHostEnvironment environment, ICurrentContext currentContext, IGlobalSettings globalSettings) { _currentContext = currentContext; _environment = environment; _pushRegistrationService = pushRegistrationService; _pushRelayer = pushRelayer; _globalSettings = globalSettings; } [HttpPost("register")] public async Task RegisterAsync([FromBody] PushRegistrationRequestModel model) { CheckUsage(); await _pushRegistrationService.CreateOrUpdateRegistrationAsync(new PushRegistrationData(model.PushToken), Prefix(model.DeviceId), Prefix(model.UserId), Prefix(model.Identifier), model.Type, model.OrganizationIds?.Select(Prefix) ?? [], model.InstallationId); } [HttpPost("delete")] public async Task DeleteAsync([FromBody] PushDeviceRequestModel model) { CheckUsage(); await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id)); } [HttpPut("add-organization")] public async Task AddOrganizationAsync([FromBody] PushUpdateRequestModel model) { CheckUsage(); await _pushRegistrationService.AddUserRegistrationOrganizationAsync( model.Devices.Select(d => Prefix(d.Id)), Prefix(model.OrganizationId)); } [HttpPut("delete-organization")] public async Task DeleteOrganizationAsync([FromBody] PushUpdateRequestModel model) { CheckUsage(); await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync( model.Devices.Select(d => Prefix(d.Id)), Prefix(model.OrganizationId)); } [HttpPost("send")] public async Task SendAsync([FromBody] PushSendRequestModel model) { CheckUsage(); NotificationTarget target; Guid targetId; if (model.InstallationId.HasValue) { if (_currentContext.InstallationId!.Value != model.InstallationId.Value) { throw new BadRequestException("InstallationId does not match current context."); } target = NotificationTarget.Installation; targetId = _currentContext.InstallationId.Value; } else if (model.UserId.HasValue) { target = NotificationTarget.User; targetId = model.UserId.Value; } else if (model.OrganizationId.HasValue) { 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) { if (string.IsNullOrWhiteSpace(value)) { return null; } return $"{_currentContext.InstallationId!.Value}_{value}"; } private void CheckUsage() { if (CanUse()) { return; } throw new BadRequestException("Not correctly configured for push relays."); } private bool CanUse() { if (_environment.IsDevelopment()) { return true; } return _currentContext.InstallationId.HasValue && !_globalSettings.SelfHosted; } }