Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 27 additions & 11 deletions src/Api/Controllers/OrganizationSponsorshipsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
Expand All @@ -16,24 +17,40 @@ namespace Bit.Api.Controllers
[Authorize("Application")]
public class OrganizationSponsorshipsController : Controller
{
private readonly IOrganizationSponsorshipService _organizationsSponsorshipService;
private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IValidateRedemptionTokenCommand _validateRedemptionTokenCommand;
private readonly IOfferSponsorshipCommand _offerSponsorshipCommand;
private readonly IResendSponsorshipOfferCommand _resendSponsorshipOfferCommand;
private readonly ISetUpSponsorshipCommand _setUpSponsorshipCommand;
private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand;
private readonly IRemoveSponsorshipCommand _removeSponsorshipCommand;
private readonly ICurrentContext _currentContext;
private readonly IUserService _userService;

public OrganizationSponsorshipsController(IOrganizationSponsorshipService organizationSponsorshipService,
public OrganizationSponsorshipsController(
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IValidateRedemptionTokenCommand validateRedemptionTokenCommand,
IOfferSponsorshipCommand offerSponsorshipCommand,
IResendSponsorshipOfferCommand resendSponsorshipOfferCommand,
ISetUpSponsorshipCommand setUpSponsorshipCommand,
IRevokeSponsorshipCommand revokeSponsorshipCommand,
IRemoveSponsorshipCommand removeSponsorshipCommand,
IUserService userService,
ICurrentContext currentContext)
{
_organizationsSponsorshipService = organizationSponsorshipService;
_organizationSponsorshipRepository = organizationSponsorshipRepository;
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
_validateRedemptionTokenCommand = validateRedemptionTokenCommand;
_offerSponsorshipCommand = offerSponsorshipCommand;
_resendSponsorshipOfferCommand = resendSponsorshipOfferCommand;
_setUpSponsorshipCommand = setUpSponsorshipCommand;
_revokeSponsorshipCommand = revokeSponsorshipCommand;
_removeSponsorshipCommand = removeSponsorshipCommand;
_userService = userService;
_currentContext = currentContext;
}
Expand All @@ -42,7 +59,7 @@ public OrganizationSponsorshipsController(IOrganizationSponsorshipService organi
[SelfHosted(NotSelfHostedOnly = true)]
public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipRequestModel model)
{
await _organizationsSponsorshipService.OfferSponsorshipAsync(
await _offerSponsorshipCommand.OfferSponsorshipAsync(
await _organizationRepository.GetByIdAsync(sponsoringOrgId),
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default),
model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName,
Expand All @@ -56,7 +73,7 @@ public async Task ResendSponsorshipOffer(Guid sponsoringOrgId)
var sponsoringOrgUser = await _organizationUserRepository
.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default);

await _organizationsSponsorshipService.ResendSponsorshipOfferAsync(
await _resendSponsorshipOfferCommand.ResendSponsorshipOfferAsync(
await _organizationRepository.GetByIdAsync(sponsoringOrgId),
sponsoringOrgUser,
await _organizationSponsorshipRepository
Expand All @@ -68,14 +85,14 @@ await _organizationSponsorshipRepository
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<bool> PreValidateSponsorshipToken([FromQuery] string sponsorshipToken)
{
return await _organizationsSponsorshipService.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email);
return await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email);
}

[HttpPost("redeem")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task RedeemSponsorship([FromQuery] string sponsorshipToken, [FromBody] OrganizationSponsorshipRedeemRequestModel model)
{
if (!await _organizationsSponsorshipService.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email))
if (!await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email))
{
throw new BadRequestException("Failed to parse sponsorship token.");
}
Expand All @@ -85,10 +102,9 @@ public async Task RedeemSponsorship([FromQuery] string sponsorshipToken, [FromBo
throw new BadRequestException("Can only redeem sponsorship for an organization you own.");
}

await _organizationsSponsorshipService.SetUpSponsorshipAsync(
await _setUpSponsorshipCommand.SetUpSponsorshipAsync(
await _organizationSponsorshipRepository
.GetByOfferedToEmailAsync((await CurrentUser).Email),
// Check org to sponsor's product type
await _organizationRepository.GetByIdAsync(model.SponsoredOrganizationId));
}

Expand All @@ -107,7 +123,7 @@ public async Task RevokeSponsorship(Guid sponsoringOrganizationId)
var existingOrgSponsorship = await _organizationSponsorshipRepository
.GetBySponsoringOrganizationUserIdAsync(orgUser.Id);

await _organizationsSponsorshipService.RevokeSponsorshipAsync(
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(
await _organizationRepository
.GetByIdAsync(existingOrgSponsorship.SponsoredOrganizationId ?? default),
existingOrgSponsorship);
Expand All @@ -127,7 +143,7 @@ public async Task RemoveSponsorship(Guid sponsoredOrgId)
var existingOrgSponsorship = await _organizationSponsorshipRepository
.GetBySponsoredOrganizationIdAsync(sponsoredOrgId);

await _organizationsSponsorshipService.RemoveSponsorshipAsync(
await _removeSponsorshipCommand.RemoveSponsorshipAsync(
await _organizationRepository
.GetByIdAsync(existingOrgSponsorship.SponsoredOrganizationId.Value),
existingOrgSponsorship);
Expand Down
10 changes: 5 additions & 5 deletions src/Billing/Controllers/StripeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
Expand All @@ -30,7 +31,7 @@ public class StripeController : Controller
private readonly BillingSettings _billingSettings;
private readonly IWebHostEnvironment _hostingEnvironment;
private readonly IOrganizationService _organizationService;
private readonly IOrganizationSponsorshipService _organizationSponsorshipService;
private readonly IValidateSponsorshipCommand _validateSponsorshipCommand;
private readonly IOrganizationRepository _organizationRepository;
private readonly ITransactionRepository _transactionRepository;
private readonly IUserService _userService;
Expand All @@ -47,7 +48,7 @@ public StripeController(
IOptions<BillingSettings> billingSettings,
IWebHostEnvironment hostingEnvironment,
IOrganizationService organizationService,
IOrganizationSponsorshipService organizationSponsorshipService,
IValidateSponsorshipCommand validateSponsorshipCommand,
IOrganizationRepository organizationRepository,
ITransactionRepository transactionRepository,
IUserService userService,
Expand All @@ -61,7 +62,7 @@ public StripeController(
_billingSettings = billingSettings?.Value;
_hostingEnvironment = hostingEnvironment;
_organizationService = organizationService;
_organizationSponsorshipService = organizationSponsorshipService;
_validateSponsorshipCommand = validateSponsorshipCommand;
_organizationRepository = organizationRepository;
_transactionRepository = transactionRepository;
_userService = userService;
Expand Down Expand Up @@ -171,8 +172,7 @@ await _userService.UpdatePremiumExpirationAsync(ids.Item2.Value,
// sponsored org
if (CheckSponsoredSubscription(subscription))
{
await _organizationSponsorshipService
.ValidateSponsorshipAsync(ids.Item1.Value);
await _validateSponsorshipCommand.ValidateSponsorshipAsync(ids.Item1.Value);
}

var org = await _organizationRepository.GetByIdAsync(ids.Item1.Value);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.Text.Json.Serialization;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Tokens;

namespace Bit.Core.Models.Business.Tokenables
{
public class OrganizationSponsorshipOfferTokenable : Tokenable
{
public const string ClearTextPrefix = "BWOrganizationSponsorship_";
public const string DataProtectorPurpose = "EmergencyAccessServiceDataProtector";
public const string TokenIdentifier = "OrganizationSponsorshipOfferToken";
public string Identifier { get; set; } = TokenIdentifier;
public Guid Id { get; set; }
public PlanSponsorshipType SponsorshipType { get; set; }
public string Email { get; set; }

public override bool Valid => Identifier == TokenIdentifier && Id != default &&
!string.IsNullOrWhiteSpace(Email);

[JsonConstructor]
public OrganizationSponsorshipOfferTokenable() { }

public OrganizationSponsorshipOfferTokenable(OrganizationSponsorship sponsorship)
{
if (sponsorship.Id == default)
{
throw new ArgumentException("Invalid OrganizationSponsorship to create a token, Id is required", nameof(sponsorship));
}
Id = sponsorship.Id;

if (string.IsNullOrWhiteSpace(sponsorship.OfferedToEmail))
{
throw new ArgumentException("Invalid OrganizationSponsorship to create a token, OfferedToEmail is required", nameof(sponsorship));
}
Email = sponsorship.OfferedToEmail;

if (!sponsorship.PlanSponsorshipType.HasValue)
{
throw new ArgumentException("Invalid OrganizationSponsorship to create a token, PlanSponsorshipType is required", nameof(sponsorship));
}
SponsorshipType = sponsorship.PlanSponsorshipType.Value;
}

public bool IsValid(OrganizationSponsorship sponsorship, string currentUserEmail) =>
sponsorship != null &&
Id == sponsorship.Id &&
sponsorship.PlanSponsorshipType.HasValue &&
SponsorshipType == sponsorship.PlanSponsorshipType.Value &&
!string.IsNullOrWhiteSpace(sponsorship.OfferedToEmail) &&
Email.Equals(currentUserEmail, StringComparison.InvariantCultureIgnoreCase) &&
Email.Equals(sponsorship.OfferedToEmail, StringComparison.InvariantCultureIgnoreCase);

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Bit.Core.Models.Business.Tokenables;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Services;
using Bit.Core.Tokens;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;

namespace Bit.Core.OrganizationFeatures
{
public static class OrganizationServiceCollectionExtensions
{
public static void AddOrganizationServices(this IServiceCollection services)
{
services.AddScoped<IOrganizationService, OrganizationService>();
services.AddTokenizers();
services.AddOrganizationSponsorshipCommands();
}

private static void AddOrganizationSponsorshipCommands(this IServiceCollection services)
{
services.AddScoped<IOfferSponsorshipCommand, OfferSponsorshipCommand>();
services.AddScoped<IRemoveSponsorshipCommand, RemoveSponsorshipCommand>();
services.AddScoped<IResendSponsorshipOfferCommand, ResendSponsorshipOfferCommand>();
services.AddScoped<IRevokeSponsorshipCommand, RevokeSponsorshipCommand>();
services.AddScoped<ISetUpSponsorshipCommand, SetUpSponsorshipCommand>();
services.AddScoped<IValidateRedemptionTokenCommand, ValidateRedemptionTokenCommand>();
services.AddScoped<IValidateSponsorshipCommand, ValidateSponsorshipCommand>();
}

private static void AddTokenizers(this IServiceCollection services)
{
services.AddSingleton<IDataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>>(serviceProvider =>
new DataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>(
OrganizationSponsorshipOfferTokenable.ClearTextPrefix,
OrganizationSponsorshipOfferTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider())
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Threading.Tasks;
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Bit.Core.Services;

namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise
{
public abstract class CancelSponsorshipCommand
{
protected readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;
protected readonly IOrganizationRepository _organizationRepository;
private readonly IPaymentService _paymentService;
private readonly IMailService _mailService;

public CancelSponsorshipCommand(IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IOrganizationRepository organizationRepository,
IPaymentService paymentService,
IMailService mailService)
{
_organizationSponsorshipRepository = organizationSponsorshipRepository;
_organizationRepository = organizationRepository;
_paymentService = paymentService;
_mailService = mailService;
}

protected async Task CancelSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship = null)
{
if (sponsoredOrganization != null)
{
await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization, sponsorship);
await _organizationRepository.UpsertAsync(sponsoredOrganization);

await _mailService.SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(
sponsoredOrganization.BillingEmailAddress(),
sponsoredOrganization.Name);
}

if (sponsorship == null)
{
return;
}

// Initialize the record as available
sponsorship.SponsoredOrganizationId = null;
sponsorship.FriendlyName = null;
sponsorship.OfferedToEmail = null;
sponsorship.PlanSponsorshipType = null;
sponsorship.TimesRenewedWithoutValidation = 0;
sponsorship.SponsorshipLapsedDate = null;

if (sponsorship.CloudSponsor || sponsorship.SponsorshipLapsedDate.HasValue)
{
await _organizationSponsorshipRepository.DeleteAsync(sponsorship);
}
else
{
await _organizationSponsorshipRepository.UpsertAsync(sponsorship);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Threading.Tasks;
using Bit.Core.Entities;
using Bit.Core.Enums;

namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces
{
public interface IOfferSponsorshipCommand
{
Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser,
PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName, string sponsoringUserEmail);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Bit.Core.Entities;

namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces
{
public interface IRemoveSponsorshipCommand
{
Task RemoveSponsorshipAsync(Organization sponsoredOrg, OrganizationSponsorship sponsorship);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Bit.Core.Entities;

namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces
{
public interface IResendSponsorshipOfferCommand
{
Task ResendSponsorshipOfferAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser,
OrganizationSponsorship sponsorship, string sponsoringUserEmail);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Bit.Core.Entities;

namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces
{
public interface IRevokeSponsorshipCommand
{
Task RevokeSponsorshipAsync(Organization sponsoredOrg, OrganizationSponsorship sponsorship);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Bit.Core.Entities;

namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces
{
public interface ISetUpSponsorshipCommand
{
Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship,
Organization sponsoredOrganization);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Threading.Tasks;

namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces
{
public interface IValidateRedemptionTokenCommand
{
Task<bool> ValidateRedemptionTokenAsync(string encryptedToken, string sponsoredUserEmail);
}
}
Loading