using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response.Organizations; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Models.Api.Request.OrganizationSponsorships; using Bit.Core.Models.Api.Response.OrganizationSponsorships; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Bit.Api.Controllers; [Route("organization/sponsorship")] public class OrganizationSponsorshipsController : Controller { private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IValidateRedemptionTokenCommand _validateRedemptionTokenCommand; private readonly IValidateBillingSyncKeyCommand _validateBillingSyncKeyCommand; private readonly ICreateSponsorshipCommand _createSponsorshipCommand; private readonly ISendSponsorshipOfferCommand _sendSponsorshipOfferCommand; private readonly ISetUpSponsorshipCommand _setUpSponsorshipCommand; private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand; private readonly IRemoveSponsorshipCommand _removeSponsorshipCommand; private readonly ICloudSyncSponsorshipsCommand _syncSponsorshipsCommand; private readonly ICurrentContext _currentContext; private readonly IUserService _userService; public OrganizationSponsorshipsController( IOrganizationSponsorshipRepository organizationSponsorshipRepository, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IValidateRedemptionTokenCommand validateRedemptionTokenCommand, IValidateBillingSyncKeyCommand validateBillingSyncKeyCommand, ICreateSponsorshipCommand createSponsorshipCommand, ISendSponsorshipOfferCommand sendSponsorshipOfferCommand, ISetUpSponsorshipCommand setUpSponsorshipCommand, IRevokeSponsorshipCommand revokeSponsorshipCommand, IRemoveSponsorshipCommand removeSponsorshipCommand, ICloudSyncSponsorshipsCommand syncSponsorshipsCommand, IUserService userService, ICurrentContext currentContext) { _organizationSponsorshipRepository = organizationSponsorshipRepository; _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; _validateRedemptionTokenCommand = validateRedemptionTokenCommand; _validateBillingSyncKeyCommand = validateBillingSyncKeyCommand; _createSponsorshipCommand = createSponsorshipCommand; _sendSponsorshipOfferCommand = sendSponsorshipOfferCommand; _setUpSponsorshipCommand = setUpSponsorshipCommand; _revokeSponsorshipCommand = revokeSponsorshipCommand; _removeSponsorshipCommand = removeSponsorshipCommand; _syncSponsorshipsCommand = syncSponsorshipsCommand; _userService = userService; _currentContext = currentContext; } [Authorize("Application")] [HttpPost("{sponsoringOrgId}/families-for-enterprise")] [SelfHosted(NotSelfHostedOnly = true)] public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipCreateRequestModel model) { var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgId); var sponsorship = await _createSponsorshipCommand.CreateSponsorshipAsync( sponsoringOrg, await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default), model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName); await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name); } [Authorize("Application")] [HttpPost("{sponsoringOrgId}/families-for-enterprise/resend")] [SelfHosted(NotSelfHostedOnly = true)] public async Task ResendSponsorshipOffer(Guid sponsoringOrgId) { var sponsoringOrgUser = await _organizationUserRepository .GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default); await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync( await _organizationRepository.GetByIdAsync(sponsoringOrgId), sponsoringOrgUser, await _organizationSponsorshipRepository .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id)); } [Authorize("Application")] [HttpPost("validate-token")] [SelfHosted(NotSelfHostedOnly = true)] public async Task PreValidateSponsorshipToken([FromQuery] string sponsorshipToken) { return (await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email)).valid; } [Authorize("Application")] [HttpPost("redeem")] [SelfHosted(NotSelfHostedOnly = true)] public async Task RedeemSponsorship([FromQuery] string sponsorshipToken, [FromBody] OrganizationSponsorshipRedeemRequestModel model) { var (valid, sponsorship) = await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email); if (!valid) { throw new BadRequestException("Failed to parse sponsorship token."); } if (!await _currentContext.OrganizationOwner(model.SponsoredOrganizationId)) { throw new BadRequestException("Can only redeem sponsorship for an organization you own."); } await _setUpSponsorshipCommand.SetUpSponsorshipAsync( sponsorship, await _organizationRepository.GetByIdAsync(model.SponsoredOrganizationId)); } [Authorize("Installation")] [HttpPost("sync")] public async Task Sync([FromBody] OrganizationSponsorshipSyncRequestModel model) { var sponsoringOrg = await _organizationRepository.GetByIdAsync(model.SponsoringOrganizationCloudId); if (!await _validateBillingSyncKeyCommand.ValidateBillingSyncKeyAsync(sponsoringOrg, model.BillingSyncKey)) { throw new BadRequestException("Invalid Billing Sync Key"); } var (syncResponseData, offersToSend) = await _syncSponsorshipsCommand.SyncOrganization(sponsoringOrg, model.ToOrganizationSponsorshipSync().SponsorshipsBatch); await _sendSponsorshipOfferCommand.BulkSendSponsorshipOfferAsync(sponsoringOrg.Name, offersToSend); return new OrganizationSponsorshipSyncResponseModel(syncResponseData); } [Authorize("Application")] [HttpDelete("{sponsoringOrganizationId}")] [HttpPost("{sponsoringOrganizationId}/delete")] [SelfHosted(NotSelfHostedOnly = true)] public async Task RevokeSponsorship(Guid sponsoringOrganizationId) { var orgUser = await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrganizationId, _currentContext.UserId ?? default); if (_currentContext.UserId != orgUser?.UserId) { throw new BadRequestException("Can only revoke a sponsorship you granted."); } var existingOrgSponsorship = await _organizationSponsorshipRepository .GetBySponsoringOrganizationUserIdAsync(orgUser.Id); await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship); } [Authorize("Application")] [HttpDelete("sponsored/{sponsoredOrgId}")] [HttpPost("sponsored/{sponsoredOrgId}/remove")] [SelfHosted(NotSelfHostedOnly = true)] public async Task RemoveSponsorship(Guid sponsoredOrgId) { if (!await _currentContext.OrganizationOwner(sponsoredOrgId)) { throw new BadRequestException("Only the owner of an organization can remove sponsorship."); } var existingOrgSponsorship = await _organizationSponsorshipRepository .GetBySponsoredOrganizationIdAsync(sponsoredOrgId); await _removeSponsorshipCommand.RemoveSponsorshipAsync(existingOrgSponsorship); } [HttpGet("{sponsoringOrgId}/sync-status")] public async Task GetSyncStatus(Guid sponsoringOrgId) { var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgId); if (!await _currentContext.OrganizationOwner(sponsoringOrg.Id)) { throw new NotFoundException(); } var lastSyncDate = await _organizationSponsorshipRepository.GetLatestSyncDateBySponsoringOrganizationIdAsync(sponsoringOrg.Id); return new OrganizationSponsorshipSyncStatusResponseModel(lastSyncDate); } private Task CurrentUser => _userService.GetUserByIdAsync(_currentContext.UserId.Value); }