Skip to content

Commit b3981a7

Browse files
Families for enterprise/split up organization sponsorship service (#1875)
* Split OrganizationSponsorshipService into commands * Use tokenable for token validation * Use interfaces to set up for DI * Use commands over services * Move service tests to command tests * Value types can't be null * Run dotnet format * Update src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommand.cs Co-authored-by: Justin Baur <admin@justinbaur.com> * Fix controller tests * Split create and send sponsorships * Split up create sponsorship * Add self hosted commands to dependency injection * Add field to store cloud billing sync key on self host instances * Fix typo * Fix data protector purpose of sponsorship offers * Split cloud and selfhosted sponsorship offer tokenable * Generate offer from self hosted with all necessary auth data * Add Required properties to constructor * Split up cancel sponsorship command * Split revoke sponsorship command between cloud and self hosted * Fix/f4e multiple sponsorships (#1838) * Use sponosorship from validate to redeem * Update tests * Format * Remove sponsorship service * Run dotnet format * Fix self hosted only controller attribute * Clean up file structure and fixes * Remove unneeded tokenables * Remove obsolete commands * Do not require file/class prefix if unnecessary * Update Organizaiton sprocs * Remove unnecessary models * Fix tests * Generalize LicenseService path calculation Use async file read and deserialization * Use interfaces for testability * Remove unused usings * Correct test direction * Test license reading * remove unused usings * Format Co-authored-by: Justin Baur <admin@justinbaur.com>
1 parent 5fef59c commit b3981a7

File tree

52 files changed

+3905
-592
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+3905
-592
lines changed

src/Api/Controllers/OrganizationSponsorshipsController.cs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ public class OrganizationSponsorshipsController : Controller
2121
private readonly IOrganizationRepository _organizationRepository;
2222
private readonly IOrganizationUserRepository _organizationUserRepository;
2323
private readonly IValidateRedemptionTokenCommand _validateRedemptionTokenCommand;
24-
private readonly IOfferSponsorshipCommand _offerSponsorshipCommand;
25-
private readonly IResendSponsorshipOfferCommand _resendSponsorshipOfferCommand;
24+
private readonly ICreateSponsorshipCommand _createSponsorshipCommand;
25+
private readonly ISendSponsorshipOfferCommand _sendSponsorshipOfferCommand;
2626
private readonly ISetUpSponsorshipCommand _setUpSponsorshipCommand;
27-
private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand;
27+
private readonly ICloudRevokeSponsorshipCommand _revokeSponsorshipCommand;
2828
private readonly IRemoveSponsorshipCommand _removeSponsorshipCommand;
2929
private readonly ICurrentContext _currentContext;
3030
private readonly IUserService _userService;
@@ -34,10 +34,10 @@ public OrganizationSponsorshipsController(
3434
IOrganizationRepository organizationRepository,
3535
IOrganizationUserRepository organizationUserRepository,
3636
IValidateRedemptionTokenCommand validateRedemptionTokenCommand,
37-
IOfferSponsorshipCommand offerSponsorshipCommand,
38-
IResendSponsorshipOfferCommand resendSponsorshipOfferCommand,
37+
ICreateSponsorshipCommand offerSponsorshipCommand,
38+
ISendSponsorshipOfferCommand sendSponsorshipOfferCommand,
3939
ISetUpSponsorshipCommand setUpSponsorshipCommand,
40-
IRevokeSponsorshipCommand revokeSponsorshipCommand,
40+
ICloudRevokeSponsorshipCommand revokeSponsorshipCommand,
4141
IRemoveSponsorshipCommand removeSponsorshipCommand,
4242
IUserService userService,
4343
ICurrentContext currentContext)
@@ -46,8 +46,8 @@ public OrganizationSponsorshipsController(
4646
_organizationRepository = organizationRepository;
4747
_organizationUserRepository = organizationUserRepository;
4848
_validateRedemptionTokenCommand = validateRedemptionTokenCommand;
49-
_offerSponsorshipCommand = offerSponsorshipCommand;
50-
_resendSponsorshipOfferCommand = resendSponsorshipOfferCommand;
49+
_createSponsorshipCommand = offerSponsorshipCommand;
50+
_sendSponsorshipOfferCommand = sendSponsorshipOfferCommand;
5151
_setUpSponsorshipCommand = setUpSponsorshipCommand;
5252
_revokeSponsorshipCommand = revokeSponsorshipCommand;
5353
_removeSponsorshipCommand = removeSponsorshipCommand;
@@ -59,11 +59,11 @@ public OrganizationSponsorshipsController(
5959
[SelfHosted(NotSelfHostedOnly = true)]
6060
public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipRequestModel model)
6161
{
62-
await _offerSponsorshipCommand.OfferSponsorshipAsync(
62+
var sponsorship = await _createSponsorshipCommand.CreateSponsorshipAsync(
6363
await _organizationRepository.GetByIdAsync(sponsoringOrgId),
6464
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default),
65-
model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName,
66-
(await CurrentUser).Email);
65+
model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName);
66+
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, (await CurrentUser).Email);
6767
}
6868

6969
[HttpPost("{sponsoringOrgId}/families-for-enterprise/resend")]
@@ -73,7 +73,7 @@ public async Task ResendSponsorshipOffer(Guid sponsoringOrgId)
7373
var sponsoringOrgUser = await _organizationUserRepository
7474
.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default);
7575

76-
await _resendSponsorshipOfferCommand.ResendSponsorshipOfferAsync(
76+
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(
7777
await _organizationRepository.GetByIdAsync(sponsoringOrgId),
7878
sponsoringOrgUser,
7979
await _organizationSponsorshipRepository
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Bit.Api.Models.Request.Organizations;
4+
using Bit.Core.Context;
5+
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
6+
using Bit.Core.Repositories;
7+
using Bit.Core.Utilities;
8+
using Microsoft.AspNetCore.Authorization;
9+
using Microsoft.AspNetCore.Mvc;
10+
11+
namespace Bit.Api.Controllers.SelfHosted
12+
{
13+
[Route("organization/sponsorship/self-hosted")]
14+
[Authorize("Application")]
15+
[SelfHosted(SelfHostedOnly = true)]
16+
public class SelfHostedOrganizationSponsorshipsController : Controller
17+
{
18+
private readonly IOrganizationRepository _organizationRepository;
19+
private readonly IOrganizationUserRepository _organizationUserRepository;
20+
private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;
21+
private readonly ICreateSponsorshipCommand _offerSponsorshipCommand;
22+
private readonly ISelfHostedRevokeSponsorshipCommand _revokeSponsorshipCommand;
23+
private readonly ICurrentContext _currentContext;
24+
25+
public SelfHostedOrganizationSponsorshipsController(
26+
ICreateSponsorshipCommand offerSponsorshipCommand,
27+
ISelfHostedRevokeSponsorshipCommand revokeSponsorshipCommand,
28+
IOrganizationRepository organizationRepository,
29+
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
30+
IOrganizationUserRepository organizationUserRepository,
31+
ICurrentContext currentContext
32+
)
33+
{
34+
_offerSponsorshipCommand = offerSponsorshipCommand;
35+
_revokeSponsorshipCommand = revokeSponsorshipCommand;
36+
_organizationRepository = organizationRepository;
37+
_organizationSponsorshipRepository = organizationSponsorshipRepository;
38+
_organizationUserRepository = organizationUserRepository;
39+
_currentContext = currentContext;
40+
}
41+
42+
[HttpPost("{sponsoringOrgId}/families-for-enterprise")]
43+
public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipRequestModel model)
44+
{
45+
await _offerSponsorshipCommand.CreateSponsorshipAsync(
46+
await _organizationRepository.GetByIdAsync(sponsoringOrgId),
47+
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default),
48+
model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName);
49+
}
50+
51+
[HttpDelete("{sponsoringOrgId}")]
52+
[HttpPost("{sponsoringOrgId}/delete")]
53+
public async Task RevokeSponsorship(Guid sponsoringOrganizationId)
54+
{
55+
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrganizationId, _currentContext.UserId ?? default);
56+
57+
var existingOrgSponsorship = await _organizationSponsorshipRepository
58+
.GetBySponsoringOrganizationUserIdAsync(orgUser.Id);
59+
60+
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
61+
}
62+
}
63+
}

src/Core/Entities/Organization.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ public class Organization : ITableObject<Guid>, ISubscriber, IStorable, IStorabl
7070
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
7171
public int? MaxAutoscaleSeats { get; set; } = null;
7272
public DateTime? OwnersNotifiedOfAutoscaling { get; set; } = null;
73+
[MaxLength(30)]
74+
public string CloudBillingSyncKey { get; set; }
7375

7476
public void SetNewId()
7577
{

src/Core/Models/Business/OrganizationLicense.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ public bool CanUse(IGlobalSettings globalSettings)
209209
}
210210
}
211211

212-
public bool VerifyData(Organization organization, GlobalSettings globalSettings)
212+
public bool VerifyData(Organization organization, IGlobalSettings globalSettings)
213213
{
214214
if (Issued > DateTime.UtcNow || Expires < DateTime.UtcNow)
215215
{

src/Core/Models/Business/Tokenables/OrganizationSponsorshipOfferTokenable.cs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,23 @@ namespace Bit.Core.Models.Business.Tokenables
99
public class OrganizationSponsorshipOfferTokenable : Tokenable
1010
{
1111
public const string ClearTextPrefix = "BWOrganizationSponsorship_";
12-
public const string DataProtectorPurpose = "EmergencyAccessServiceDataProtector";
12+
public const string DataProtectorPurpose = "OrganizationSponsorshipDataProtector";
1313
public const string TokenIdentifier = "OrganizationSponsorshipOfferToken";
1414
public string Identifier { get; set; } = TokenIdentifier;
1515
public Guid Id { get; set; }
1616
public PlanSponsorshipType SponsorshipType { get; set; }
1717
public string Email { get; set; }
1818

19-
public override bool Valid => Identifier == TokenIdentifier && Id != default &&
20-
!string.IsNullOrWhiteSpace(Email);
19+
public override bool Valid => !string.IsNullOrWhiteSpace(Email) &&
20+
Identifier == TokenIdentifier &&
21+
Id != default;
22+
2123

2224
[JsonConstructor]
2325
public OrganizationSponsorshipOfferTokenable() { }
2426

2527
public OrganizationSponsorshipOfferTokenable(OrganizationSponsorship sponsorship)
2628
{
27-
if (sponsorship.Id == default)
28-
{
29-
throw new ArgumentException("Invalid OrganizationSponsorship to create a token, Id is required", nameof(sponsorship));
30-
}
31-
Id = sponsorship.Id;
32-
3329
if (string.IsNullOrWhiteSpace(sponsorship.OfferedToEmail))
3430
{
3531
throw new ArgumentException("Invalid OrganizationSponsorship to create a token, OfferedToEmail is required", nameof(sponsorship));
@@ -41,13 +37,19 @@ public OrganizationSponsorshipOfferTokenable(OrganizationSponsorship sponsorship
4137
throw new ArgumentException("Invalid OrganizationSponsorship to create a token, PlanSponsorshipType is required", nameof(sponsorship));
4238
}
4339
SponsorshipType = sponsorship.PlanSponsorshipType.Value;
40+
41+
if (sponsorship.Id == default)
42+
{
43+
throw new ArgumentException("Invalid OrganizationSponsorship to create a token, Id is required", nameof(sponsorship));
44+
}
45+
Id = sponsorship.Id;
4446
}
4547

4648
public bool IsValid(OrganizationSponsorship sponsorship, string currentUserEmail) =>
4749
sponsorship != null &&
48-
Id == sponsorship.Id &&
4950
sponsorship.PlanSponsorshipType.HasValue &&
5051
SponsorshipType == sponsorship.PlanSponsorshipType.Value &&
52+
Id == sponsorship.Id &&
5153
!string.IsNullOrWhiteSpace(sponsorship.OfferedToEmail) &&
5254
Email.Equals(currentUserEmail, StringComparison.InvariantCultureIgnoreCase) &&
5355
Email.Equals(sponsorship.OfferedToEmail, StringComparison.InvariantCultureIgnoreCase);

src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
using Bit.Core.Models.Business.Tokenables;
22
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;
3+
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;
34
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
5+
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted;
46
using Bit.Core.Services;
7+
using Bit.Core.Settings;
58
using Bit.Core.Tokens;
69
using Microsoft.AspNetCore.DataProtection;
710
using Microsoft.Extensions.DependencyInjection;
@@ -19,12 +22,19 @@ public static void AddOrganizationServices(this IServiceCollection services)
1922

2023
private static void AddOrganizationSponsorshipCommands(this IServiceCollection services)
2124
{
22-
services.AddScoped<IOfferSponsorshipCommand, OfferSponsorshipCommand>();
25+
services.AddScoped<ICreateSponsorshipCommand, CreateSponsorshipCommand>();
26+
2327
services.AddScoped<IRemoveSponsorshipCommand, RemoveSponsorshipCommand>();
24-
services.AddScoped<IResendSponsorshipOfferCommand, ResendSponsorshipOfferCommand>();
25-
services.AddScoped<IRevokeSponsorshipCommand, RevokeSponsorshipCommand>();
28+
29+
services.AddScoped<ISendSponsorshipOfferCommand, SendSponsorshipOfferCommand>();
30+
31+
services.AddScoped<ICloudRevokeSponsorshipCommand, CloudRevokeSponsorshipCommand>();
32+
services.AddScoped<ISelfHostedRevokeSponsorshipCommand, SelfHostedRevokeSponsorshipCommand>();
33+
2634
services.AddScoped<ISetUpSponsorshipCommand, SetUpSponsorshipCommand>();
35+
2736
services.AddScoped<IValidateRedemptionTokenCommand, ValidateRedemptionTokenCommand>();
37+
2838
services.AddScoped<IValidateSponsorshipCommand, ValidateSponsorshipCommand>();
2939
}
3040

src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommand.cs

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,20 @@
55

66
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise
77
{
8-
public abstract class CancelSponsorshipCommand
8+
public class CancelSponsorshipCommand
99
{
1010
protected readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;
1111
protected readonly IOrganizationRepository _organizationRepository;
12-
private readonly IPaymentService _paymentService;
13-
private readonly IMailService _mailService;
1412

1513
public CancelSponsorshipCommand(IOrganizationSponsorshipRepository organizationSponsorshipRepository,
16-
IOrganizationRepository organizationRepository,
17-
IPaymentService paymentService,
18-
IMailService mailService)
14+
IOrganizationRepository organizationRepository)
1915
{
2016
_organizationSponsorshipRepository = organizationSponsorshipRepository;
2117
_organizationRepository = organizationRepository;
22-
_paymentService = paymentService;
23-
_mailService = mailService;
2418
}
2519

26-
protected async Task CancelSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship = null)
20+
protected virtual async Task CancelSponsorshipAsync(OrganizationSponsorship sponsorship = null)
2721
{
28-
if (sponsoredOrganization != null)
29-
{
30-
await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization, sponsorship);
31-
await _organizationRepository.UpsertAsync(sponsoredOrganization);
32-
33-
await _mailService.SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(
34-
sponsoredOrganization.BillingEmailAddress(),
35-
sponsoredOrganization.Name);
36-
}
37-
3822
if (sponsorship == null)
3923
{
4024
return;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System.Threading.Tasks;
2+
using Bit.Core.Entities;
3+
using Bit.Core.Repositories;
4+
using Bit.Core.Services;
5+
6+
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud
7+
{
8+
public abstract class CloudCancelSponsorshipCommand : CancelSponsorshipCommand
9+
{
10+
private readonly IPaymentService _paymentService;
11+
private readonly IMailService _mailService;
12+
13+
public CloudCancelSponsorshipCommand(IOrganizationSponsorshipRepository organizationSponsorshipRepository,
14+
IOrganizationRepository organizationRepository,
15+
IPaymentService paymentService,
16+
IMailService mailService) : base(organizationSponsorshipRepository, organizationRepository)
17+
{
18+
_paymentService = paymentService;
19+
_mailService = mailService;
20+
}
21+
22+
23+
protected async Task CancelSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship = null)
24+
{
25+
if (sponsoredOrganization != null)
26+
{
27+
await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization, sponsorship);
28+
await _organizationRepository.UpsertAsync(sponsoredOrganization);
29+
30+
await _mailService.SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(
31+
sponsoredOrganization.BillingEmailAddress(),
32+
sponsoredOrganization.Name);
33+
}
34+
await base.CancelSponsorshipAsync(sponsorship);
35+
}
36+
}
37+
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
using Bit.Core.Repositories;
66
using Bit.Core.Services;
77

8-
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise
8+
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud
99
{
10-
public class RevokeSponsorshipCommand : CancelSponsorshipCommand, IRevokeSponsorshipCommand
10+
public class CloudRevokeSponsorshipCommand : CloudCancelSponsorshipCommand, ICloudRevokeSponsorshipCommand
1111
{
12-
public RevokeSponsorshipCommand(
12+
public CloudRevokeSponsorshipCommand(
1313
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
1414
IOrganizationRepository organizationRepository,
1515
IPaymentService paymentService,
@@ -26,7 +26,7 @@ public async Task RevokeSponsorshipAsync(Organization sponsoredOrg, Organization
2626

2727
if (sponsorship.SponsoredOrganizationId == null)
2828
{
29-
await CancelSponsorshipAsync(null, sponsorship);
29+
await CancelSponsorshipAsync(sponsorship);
3030
return;
3131
}
3232

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
using Bit.Core.Repositories;
66
using Bit.Core.Services;
77

8-
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise
8+
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud
99
{
10-
public class RemoveSponsorshipCommand : CancelSponsorshipCommand, IRemoveSponsorshipCommand
10+
public class RemoveSponsorshipCommand : CloudCancelSponsorshipCommand, IRemoveSponsorshipCommand
1111
{
1212
public RemoveSponsorshipCommand(
1313
IOrganizationSponsorshipRepository organizationSponsorshipRepository,

0 commit comments

Comments
 (0)