1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-01 08:02:49 -05:00

Families for enterprise/stripe integrations (#1699)

* Add PlanSponsorshipType to static store

* Add sponsorship type to token and creates sponsorship

* PascalCase properties

* Require sponsorship for remove

* Create subscription sponsorship helper class

* Handle Sponsored subscription changes

* Add sponsorship id to subscription metadata

* Make sponsoring references nullable

This state indicates that a sponsorship has lapsed, but was not able to
be reverted for billing reasons

* WIP: Validate and remove subscriptions

* Update sponsorships on organization and org user delete

* Add friendly name to organization sponsorship
This commit is contained in:
Matt Gibson
2021-11-08 17:01:09 -06:00
committed by Justin Baur
parent 143be4273b
commit 45f6ec1781
42 changed files with 1060 additions and 188 deletions

View File

@ -42,8 +42,11 @@ namespace Bit.Api.Controllers
{
// TODO: validate has right to sponsor, send sponsorship email
var sponsoringOrgIdGuid = new Guid(sponsoringOrgId);
var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(model.PlanSponsorshipType)?.SponsoringProductType;
var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgIdGuid);
if (sponsoringOrg == null || !PlanTypeHelper.HasEnterprisePlan(sponsoringOrg))
if (requiredSponsoringProductType == null ||
sponsoringOrg == null ||
StaticStore.GetPlan(sponsoringOrg.PlanType).Product != requiredSponsoringProductType.Value)
{
throw new BadRequestException("Specified Organization cannot sponsor other organizations.");
}
@ -64,14 +67,14 @@ namespace Bit.Api.Controllers
throw new BadRequestException("Can only sponsor one organization per Organization User.");
}
await _organizationsSponsorshipService.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, model.PlanSponsorshipType, model.sponsoredEmail);
await _organizationsSponsorshipService.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName);
}
[HttpPost("sponsored/redeem/families-for-enterprise")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task RedeemSponsorship([FromQuery] string sponsorshipToken, [FromBody] OrganizationSponsorshipRedeemRequestModel model)
{
// TODO: parse out sponsorshipInfo
if (!await _organizationsSponsorshipService.ValidateRedemptionTokenAsync(sponsorshipToken))
{
throw new BadRequestException("Failed to parse sponsorship token.");
@ -99,9 +102,12 @@ namespace Bit.Api.Controllers
throw new BadRequestException("Cannot redeem a sponsorship offer for an organization that is already sponsored. Revoke existing sponsorship first.");
}
// Check org to sponsor's product type
var requiredSponsoredProductType = StaticStore.GetSponsoredPlan(model.PlanSponsorshipType)?.SponsoredProductType;
var organizationToSponsor = await _organizationRepository.GetByIdAsync(model.SponsoredOrganizationId);
// TODO: only current families plan?
if (organizationToSponsor == null || !PlanTypeHelper.HasFamiliesPlan(organizationToSponsor))
if (requiredSponsoredProductType == null ||
organizationToSponsor == null ||
StaticStore.GetPlan(organizationToSponsor.PlanType).Product != requiredSponsoredProductType.Value)
{
throw new BadRequestException("Can only redeem sponsorship offer on families organizations.");
}
@ -124,12 +130,19 @@ namespace Bit.Api.Controllers
var existingOrgSponsorship = await _organizationSponsorshipRepository
.GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUserIdGuid);
if (existingOrgSponsorship == null)
if (existingOrgSponsorship == null || existingOrgSponsorship.SponsoredOrganizationId == null)
{
throw new BadRequestException("You are not currently sponsoring and organization.");
throw new BadRequestException("You are not currently sponsoring an organization.");
}
await _organizationsSponsorshipService.RemoveSponsorshipAsync(existingOrgSponsorship);
var sponsoredOrganization = await _organizationRepository
.GetByIdAsync(existingOrgSponsorship.SponsoredOrganizationId.Value);
if (sponsoredOrganization == null)
{
throw new BadRequestException("Unable to find the sponsored Organization.");
}
await _organizationsSponsorshipService.RemoveSponsorshipAsync(sponsoredOrganization, existingOrgSponsorship);
}
[HttpDelete("sponsored/{sponsoredOrgId}")]
@ -146,12 +159,20 @@ namespace Bit.Api.Controllers
var existingOrgSponsorship = await _organizationSponsorshipRepository
.GetBySponsoredOrganizationIdAsync(sponsoredOrgIdGuid);
if (existingOrgSponsorship == null)
if (existingOrgSponsorship == null || existingOrgSponsorship.SponsoredOrganizationId == null)
{
throw new BadRequestException("The requested organization is not currently being sponsored.");
}
await _organizationsSponsorshipService.RemoveSponsorshipAsync(existingOrgSponsorship);
var sponsoredOrganization = await _organizationRepository
.GetByIdAsync(existingOrgSponsorship.SponsoredOrganizationId.Value);
if (sponsoredOrganization == null)
{
throw new BadRequestException("Unable to find the sponsored Organization.");
}
await _organizationsSponsorshipService.RemoveSponsorshipAsync(sponsoredOrganization, existingOrgSponsorship);
}
}
}

View File

@ -29,6 +29,7 @@ namespace Bit.Billing.Controllers
private readonly BillingSettings _billingSettings;
private readonly IWebHostEnvironment _hostingEnvironment;
private readonly IOrganizationService _organizationService;
private readonly IOrganizationSponsorshipService _organizationSponsorshipService;
private readonly IOrganizationRepository _organizationRepository;
private readonly ITransactionRepository _transactionRepository;
private readonly IUserService _userService;
@ -45,6 +46,7 @@ namespace Bit.Billing.Controllers
IOptions<BillingSettings> billingSettings,
IWebHostEnvironment hostingEnvironment,
IOrganizationService organizationService,
IOrganizationSponsorshipService organizationSponsorshipService,
IOrganizationRepository organizationRepository,
ITransactionRepository transactionRepository,
IUserService userService,
@ -58,6 +60,7 @@ namespace Bit.Billing.Controllers
_billingSettings = billingSettings?.Value;
_hostingEnvironment = hostingEnvironment;
_organizationService = organizationService;
_organizationSponsorshipService = organizationSponsorshipService;
_organizationRepository = organizationRepository;
_transactionRepository = transactionRepository;
_userService = userService;
@ -136,6 +139,16 @@ namespace Bit.Billing.Controllers
// org
if (ids.Item1.HasValue)
{
var newEndPeriod = subscription.CurrentPeriodEnd;
// sponsored org
if (IsSponsoredSubscription(subscription))
{
var sponsorshipValid = await _organizationSponsorshipService
.ValidateSponsorshipAsync(ids.Item1.Value);
// TODO: How do we return from this?
}
await _organizationService.UpdateExpirationDateAsync(ids.Item1.Value,
subscription.CurrentPeriodEnd);
}
@ -783,5 +796,8 @@ namespace Bit.Billing.Controllers
}
return subscription;
}
private static bool IsSponsoredSubscription(Subscription subscription) =>
StaticStore.SponsoredPlans.Any(p => p.StripePlanId == subscription.Id);
}
}

View File

@ -22,5 +22,7 @@ namespace Bit.Core.Enums
GoogleInApp = 7,
[Display(Name = "Check")]
Check = 8,
[Display(Name = "None")]
None = 255,
}
}

View File

@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Enums
{

View File

@ -1,6 +1,4 @@
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Bit.Core.Models.Table;
namespace Bit.Core.Enums
{
@ -29,26 +27,6 @@ namespace Bit.Core.Enums
[Display(Name = "Enterprise (Monthly)")]
EnterpriseMonthly = 10,
[Display(Name = "Enterprise (Annually)")]
EnterpriseAnnually= 11,
}
public static class PlanTypeHelper
{
private static readonly PlanType[] _freePlans = new[] { PlanType.Free };
private static readonly PlanType[] _familiesPlans = new[] { PlanType.FamiliesAnnually, PlanType.FamiliesAnnually2019 };
private static readonly PlanType[] _teamsPlans = new[] { PlanType.TeamsAnnually, PlanType.TeamsAnnually2019,
PlanType.TeamsMonthly, PlanType.TeamsMonthly2019};
private static readonly PlanType[] _enterprisePlans = new[] { PlanType.EnterpriseAnnually,
PlanType.EnterpriseAnnually2019, PlanType.EnterpriseMonthly, PlanType.EnterpriseMonthly2019 };
private static bool HasPlan(PlanType[] planTypes, PlanType planType) => planTypes.Any(p => p == planType);
public static bool HasFreePlan(Organization org) => IsFree(org.PlanType);
public static bool IsFree(PlanType planType) => HasPlan(_freePlans, planType);
public static bool HasFamiliesPlan(Organization org) => IsFamilies(org.PlanType);
public static bool IsFamilies(PlanType planType) => HasPlan(_familiesPlans, planType);
public static bool HasTeamsPlan(Organization org) => IsTeams(org.PlanType);
public static bool IsTeams(PlanType planType) => HasPlan(_teamsPlans, planType);
public static bool HasEnterprisePlan(Organization org) => IsEnterprise(org.PlanType);
public static bool IsEnterprise(PlanType planType) => HasPlan(_enterprisePlans, planType);
EnterpriseAnnually = 11,
}
}

View File

@ -1,10 +1,13 @@
using System;
using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
namespace Bit.Core.Models.Api
{
public class OrganizationSponsorshipRedeemRequestModel
{
[Required]
public PlanSponsorshipType PlanSponsorshipType { get; set; }
[Required]
public Guid SponsoredOrganizationId { get; set; }
}

View File

@ -16,6 +16,9 @@ namespace Bit.Core.Models.Api.Request
[Required]
[StringLength(256)]
[StrictEmailAddress]
public string sponsoredEmail { get; set; }
public string SponsoredEmail { get; set; }
[StringLength(256)]
public string FriendlyName { get; set; }
}
}

View File

@ -38,6 +38,7 @@ namespace Bit.Core.Models.Api
UserId = organization.UserId?.ToString();
ProviderId = organization.ProviderId?.ToString();
ProviderName = organization.ProviderName;
FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName;
}
public string Id { get; set; }
@ -68,5 +69,6 @@ namespace Bit.Core.Models.Api
public bool HasPublicAndPrivateKeys { get; set; }
public string ProviderId { get; set; }
public string ProviderName { get; set; }
public string FamilySponsorshipFriendlyName { get; set; }
}
}

View File

@ -0,0 +1,39 @@
using System.Collections.Generic;
using Bit.Core.Models.Table;
namespace Bit.Core.Models.Business
{
public class SponsoredOrganizationSubscription
{
public const string OrganizationSponsorhipIdMetadataKey = "OrganizationSponsorshipId";
private readonly string _customerId;
private readonly Organization _org;
private readonly StaticStore.Plan _plan;
private readonly List<Stripe.TaxRate> _taxRates;
public SponsoredOrganizationSubscription(Organization org, Stripe.Subscription existingSubscription)
{
_org = org;
_customerId = org.GatewayCustomerId;
_plan = Utilities.StaticStore.GetPlan(org.PlanType);
_taxRates = existingSubscription.DefaultTaxRates;
}
public SponsorOrganizationSubscriptionOptions GetSponsorSubscriptionOptions(OrganizationSponsorship sponsorship,
int additionalSeats = 0, int additionalStorageGb = 0, bool premiumAccessAddon = false)
{
var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value);
var subCreateOptions = new SponsorOrganizationSubscriptionOptions(_customerId, _org, _plan,
sponsoredPlan, _taxRates, additionalSeats, additionalStorageGb, premiumAccessAddon);
subCreateOptions.Metadata.Add(OrganizationSponsorhipIdMetadataKey, sponsorship.Id.ToString());
return subCreateOptions;
}
public OrganizationUpgradeSubscriptionOptions RemoveOrganizationSubscriptionOptions(int additionalSeats = 0,
int additionalStorageGb = 0, bool premiumAccessAddon = false) =>
new OrganizationUpgradeSubscriptionOptions(_customerId, _org, _plan, _taxRates,
additionalSeats, additionalStorageGb, premiumAccessAddon);
}
}

View File

@ -1,12 +1,14 @@
using Bit.Core.Models.Table;
using Stripe;
using System.Collections.Generic;
using System.Linq;
namespace Bit.Core.Models.Business
{
public class OrganizationSubscriptionOptionsBase : Stripe.SubscriptionCreateOptions
{
public OrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan, TaxInfo taxInfo, int additionalSeats, int additionalStorageGb, bool premiumAccessAddon)
public OrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan,
int additionalSeats, int additionalStorageGb, bool premiumAccessAddon)
{
Items = new List<SubscriptionItemOptions>();
Metadata = new Dictionary<string, string>
@ -14,15 +16,6 @@ namespace Bit.Core.Models.Business
[org.GatewayIdField()] = org.Id.ToString()
};
if (plan.StripePlanId != null)
{
Items.Add(new SubscriptionItemOptions
{
Plan = plan.StripePlanId,
Quantity = 1
});
}
if (additionalSeats > 0 && plan.StripeSeatPlanId != null)
{
Items.Add(new SubscriptionItemOptions
@ -49,15 +42,53 @@ namespace Bit.Core.Models.Business
Quantity = 1
});
}
}
if (!string.IsNullOrWhiteSpace(taxInfo?.StripeTaxRateId))
protected void AddPlanItem(StaticStore.Plan plan) => AddPlanItem(plan.StripePlanId);
protected void AddPlanItem(StaticStore.SponsoredPlan sponsoredPlan) => AddPlanItem(sponsoredPlan.StripePlanId);
protected void AddPlanItem(string stripePlanId)
{
if (stripePlanId != null)
{
DefaultTaxRates = new List<string>{ taxInfo.StripeTaxRateId };
Items.Add(new SubscriptionItemOptions
{
Plan = stripePlanId,
Quantity = 1,
});
}
}
protected void AddTaxRateItem(TaxInfo taxInfo) => AddTaxRateItem(new List<string> { taxInfo.StripeTaxRateId });
protected void AddTaxRateItem(List<Stripe.TaxRate> taxRates) => AddTaxRateItem(taxRates?.Select(t => t.Id).ToList());
protected void AddTaxRateItem(List<string> taxRateIds)
{
if (taxRateIds != null && taxRateIds.Any())
{
DefaultTaxRates = taxRateIds;
}
}
}
public class OrganizationPurchaseSubscriptionOptions : OrganizationSubscriptionOptionsBase
public abstract class UnsponsoredOrganizationSubscriptionOptionsBase : OrganizationSubscriptionOptionsBase
{
public UnsponsoredOrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan, TaxInfo taxInfo,
int additionalSeats, int additionalStorage, bool premiumAccessAddon) :
base(org, plan, additionalSeats, additionalStorage, premiumAccessAddon)
{
AddPlanItem(plan);
AddTaxRateItem(taxInfo);
}
public UnsponsoredOrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan, List<Stripe.TaxRate> taxInfo,
int additionalSeats, int additionalStorage, bool premiumAccessAddon) :
base(org, plan, additionalSeats, additionalStorage, premiumAccessAddon)
{
AddPlanItem(plan);
AddTaxRateItem(taxInfo);
}
}
public class OrganizationPurchaseSubscriptionOptions : UnsponsoredOrganizationSubscriptionOptionsBase
{
public OrganizationPurchaseSubscriptionOptions(
Organization org, StaticStore.Plan plan,
@ -70,7 +101,7 @@ namespace Bit.Core.Models.Business
}
}
public class OrganizationUpgradeSubscriptionOptions : OrganizationSubscriptionOptionsBase
public class OrganizationUpgradeSubscriptionOptions : UnsponsoredOrganizationSubscriptionOptionsBase
{
public OrganizationUpgradeSubscriptionOptions(
string customerId, Organization org,
@ -81,5 +112,43 @@ namespace Bit.Core.Models.Business
{
Customer = customerId;
}
public OrganizationUpgradeSubscriptionOptions(
string customerId, Organization org,
StaticStore.Plan plan, List<Stripe.TaxRate> taxInfo,
int additionalSeats = 0, int additionalStorageGb = 0,
bool premiumAccessAddon = false) :
base(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon)
{
Customer = customerId;
}
}
public class RemoveOrganizationSubscriptionOptions : OrganizationSubscriptionOptionsBase
{
public RemoveOrganizationSubscriptionOptions(string customerId, Organization org,
StaticStore.Plan plan, List<string> existingTaxRateStripeIds,
int additionalSeats = 0, int additionalStorageGb = 0,
bool premiumAccessAddon = false) :
base(org, plan, additionalSeats, additionalStorageGb, premiumAccessAddon)
{
Customer = customerId;
AddPlanItem(plan);
AddTaxRateItem(existingTaxRateStripeIds);
}
}
public class SponsorOrganizationSubscriptionOptions : OrganizationSubscriptionOptionsBase
{
public SponsorOrganizationSubscriptionOptions(
string customerId, Organization org, StaticStore.Plan existingPlan,
StaticStore.SponsoredPlan sponsorshipPlan, List<Stripe.TaxRate> existingTaxRates, int additionalSeats = 0,
int additionalStorageGb = 0, bool premiumAccessAddon = false) :
base(org, existingPlan, additionalSeats, additionalStorageGb, premiumAccessAddon)
{
Customer = customerId;
AddPlanItem(sponsorshipPlan);
AddTaxRateItem(existingTaxRates);
}
}
}

View File

@ -33,5 +33,6 @@ namespace Bit.Core.Models.Data
public string PrivateKey { get; set; }
public Guid? ProviderId { get; set; }
public string ProviderName { get; set; }
public string FamilySponsorshipFriendlyName { get; set; }
}
}

View File

@ -0,0 +1,12 @@
using Bit.Core.Enums;
namespace Bit.Core.Models.StaticStore
{
public class SponsoredPlan
{
public PlanSponsorshipType PlanSponsorshipType { get; set; }
public ProductType SponsoredProductType { get; set; }
public ProductType SponsoringProductType { get; set; }
public string StripePlanId { get; set; }
}
}

View File

@ -9,12 +9,12 @@ namespace Bit.Core.Models.Table
{
public Guid Id { get; set; }
public Guid? InstallationId { get; set; }
[Required]
public Guid SponsoringOrganizationId { get; set; }
[Required]
public Guid SponsoringOrganizationUserId { get; set; }
public Guid? SponsoringOrganizationId { get; set; }
public Guid? SponsoringOrganizationUserId { get; set; }
public Guid? SponsoredOrganizationId { get; set; }
[MaxLength(256)]
public string FriendlyName { get; set; }
[MaxLength(256)]
public string OfferedToEmail { get; set; }
public PlanSponsorshipType? PlanSponsorshipType { get; set; }
[Required]

View File

@ -26,7 +26,7 @@ namespace Bit.Core.Repositories.EntityFramework
public DbSet<GroupUser> GroupUsers { get; set; }
public DbSet<Installation> Installations { get; set; }
public DbSet<Organization> Organizations { get; set; }
public DbSet<OrganizationSponsorship> organizationSponsorships { get; set; }
public DbSet<OrganizationSponsorship> OrganizationSponsorships { get; set; }
public DbSet<OrganizationUser> OrganizationUsers { get; set; }
public DbSet<Policy> Policies { get; set; }
public DbSet<Provider> Providers { get; set; }

View File

@ -95,5 +95,29 @@ namespace Bit.Core.Repositories.EntityFramework
{
await OrganizationUpdateStorage(id);
}
public override async Task DeleteAsync(Organization organization)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var orgUser = dbContext.FindAsync<EFModel.Organization>(organization.Id);
var sponsorships = dbContext.OrganizationSponsorships
.Where(os =>
os.SponsoringOrganizationId == organization.Id ||
os.SponsoredOrganizationId == organization.Id);
dbContext.RemoveRange(sponsorships.Where(os => os.CloudSponsor));
Guid? UpdatedOrgId(Guid? orgId) => orgId == organization.Id ? null : organization.Id;
foreach (var sponsorship in sponsorships.Where(os => !os.CloudSponsor))
{
sponsorship.SponsoredOrganizationId = UpdatedOrgId(sponsorship.SponsoredOrganizationId);
sponsorship.SponsoringOrganizationId = UpdatedOrgId(sponsorship.SponsoringOrganizationId);
}
dbContext.Remove(orgUser);
await dbContext.SaveChangesAsync();
}
}
}
}

View File

@ -13,7 +13,7 @@ namespace Bit.Core.Repositories.EntityFramework
public class OrganizationSponsorshipRepository : Repository<TableModel.OrganizationSponsorship, EFModel.OrganizationSponsorship, Guid>, IOrganizationSponsorshipRepository
{
public OrganizationSponsorshipRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) :
base(serviceScopeFactory, mapper, (DatabaseContext context) => context.organizationSponsorships)
base(serviceScopeFactory, mapper, (DatabaseContext context) => context.OrganizationSponsorships)
{
}

View File

@ -67,12 +67,32 @@ namespace Bit.Core.Repositories.EntityFramework
return organizationUsers.Select(u => u.Id).ToList();
}
public override async Task DeleteAsync(OrganizationUser organizationUser) => await DeleteAsync(organizationUser.Id);
public async Task DeleteAsync(Guid organizationUserId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var orgUser = dbContext.FindAsync<EfModel.OrganizationUser>(organizationUserId);
var sponsorships = dbContext.OrganizationSponsorships
.Where(os => os.SponsoringOrganizationUserId != default &&
os.SponsoringOrganizationUserId.Value == organizationUserId);
dbContext.RemoveRange(sponsorships);
dbContext.Remove(orgUser);
await dbContext.SaveChangesAsync();
}
}
public async Task DeleteManyAsync(IEnumerable<Guid> organizationUserIds)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var entities = dbContext.FindAsync<EfModel.OrganizationUser>(organizationUserIds);
var sponsorships = dbContext.OrganizationSponsorships
.Where(os => os.SponsoringOrganizationUserId != default &&
organizationUserIds.Contains(os.SponsoringOrganizationUserId ?? default));
dbContext.RemoveRange(sponsorships);
dbContext.RemoveRange(entities);
await dbContext.SaveChangesAsync();
}

View File

@ -16,8 +16,10 @@ namespace Bit.Core.Repositories.EntityFramework.Queries
from po in po_g.DefaultIfEmpty()
join p in dbContext.Providers on po.ProviderId equals p.Id into p_g
from p in p_g.DefaultIfEmpty()
join os in dbContext.OrganizationSponsorships on ou.Id equals os.SponsoringOrganizationUserId into os_g
from os in os_g.DefaultIfEmpty()
where ((su == null || !su.OrganizationId.HasValue) || su.OrganizationId == ou.OrganizationId)
select new { ou, o, su, p };
select new { ou, o, su, p, os };
return query.Select(x => new OrganizationUserOrganizationDetails
{
OrganizationId = x.ou.OrganizationId,
@ -48,6 +50,7 @@ namespace Bit.Core.Repositories.EntityFramework.Queries
PrivateKey = x.o.PrivateKey,
ProviderId = x.p.Id,
ProviderName = x.p.Name,
FamilySponsorshipFriendlyName = x.os.FriendlyName
});
}
}

View File

@ -1,3 +1,4 @@
using System;
using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Models.Table;
@ -7,8 +8,10 @@ namespace Bit.Core.Services
public interface IOrganizationSponsorshipService
{
Task<bool> ValidateRedemptionTokenAsync(string encryptedToken);
Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, PlanSponsorshipType sponsorshipType, string sponsoredEmail);
Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser,
PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName);
Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, Organization sponsoredOrganization);
Task RemoveSponsorshipAsync(OrganizationSponsorship sponsorship);
Task<bool> ValidateSponsorshipAsync(Guid sponsoredOrganizationId);
Task RemoveSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship);
}
}

View File

@ -2,6 +2,7 @@
using System.Threading.Tasks;
using Bit.Core.Models.Table;
using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
using Bit.Core.Enums;
namespace Bit.Core.Services
@ -10,13 +11,15 @@ namespace Bit.Core.Services
{
Task CancelAndRecoverChargesAsync(ISubscriber subscriber);
Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
string paymentToken, Models.StaticStore.Plan plan, short additionalStorageGb, int additionalSeats,
string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats,
bool premiumAccessAddon, TaxInfo taxInfo);
Task<string> UpgradeFreeOrganizationAsync(Organization org, Models.StaticStore.Plan plan,
Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship);
Task<bool> RemoveOrganizationSponsorshipAsync(Organization org);
Task<string> UpgradeFreeOrganizationAsync(Organization org, Plan plan,
short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo);
Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
short additionalStorageGb, TaxInfo taxInfo);
Task<string> AdjustSeatsAsync(Organization organization, Models.StaticStore.Plan plan, int additionalSeats, DateTime? prorationDate = null);
Task<string> AdjustSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null);
Task<string> AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId, DateTime? prorationDate = null);
Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false,
bool skipInAppPurchaseCheck = false);

View File

@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
using Microsoft.AspNetCore.DataProtection;
@ -13,12 +14,18 @@ namespace Bit.Core.Services
private const string TokenClearTextPrefix = "BWOrganizationSponsorship_";
private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IPaymentService _paymentService;
private readonly IDataProtector _dataProtector;
public OrganizationSponsorshipService(IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IOrganizationRepository organizationRepository,
IPaymentService paymentService,
IDataProtector dataProtector)
{
_organizationSponsorshipRepository = organizationSponsorshipRepository;
_organizationRepository = organizationRepository;
_paymentService = paymentService;
_dataProtector = dataProtector;
}
@ -63,13 +70,16 @@ namespace Bit.Core.Services
_dataProtector.Protect($"{FamiliesForEnterpriseTokenName} {sponsorshipId} {sponsorshipType}")
);
public async Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, PlanSponsorshipType sponsorshipType, string sponsoredEmail)
public async Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser,
PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName)
{
var sponsorship = new OrganizationSponsorship
{
SponsoringOrganizationId = sponsoringOrg.Id,
SponsoringOrganizationUserId = sponsoringOrgUser.Id,
FriendlyName = friendlyName,
OfferedToEmail = sponsoredEmail,
PlanSponsorshipType = sponsorshipType,
CloudSponsor = true,
};
@ -78,6 +88,7 @@ namespace Bit.Core.Services
sponsorship = await _organizationSponsorshipRepository.CreateAsync(sponsorship);
// TODO: send email to sponsoredEmail w/ redemption token link
var _ = RedemptionToken(sponsorship.Id, sponsorshipType);
}
catch
{
@ -91,14 +102,117 @@ namespace Bit.Core.Services
public async Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, Organization sponsoredOrganization)
{
// TODO: set up sponsorship, remember remove offeredToEmail from sponsorship
throw new NotImplementedException();
if (sponsorship.PlanSponsorshipType == null)
{
throw new BadRequestException("Cannot set up sponsorship without a known sponsorship type.");
}
// TODO: rollback?
await _paymentService.SponsorOrganizationAsync(sponsoredOrganization, sponsorship);
await _organizationRepository.UpsertAsync(sponsoredOrganization);
sponsorship.SponsoredOrganizationId = sponsoredOrganization.Id;
sponsorship.OfferedToEmail = null;
await _organizationSponsorshipRepository.UpsertAsync(sponsorship);
}
public async Task RemoveSponsorshipAsync(OrganizationSponsorship sponsorship)
public async Task<bool> ValidateSponsorshipAsync(Guid sponsoredOrganizationId)
{
// TODO: remove sponsorship
throw new NotImplementedException();
var sponsoredOrganization = await _organizationRepository.GetByIdAsync(sponsoredOrganizationId);
var existingSponsorship = await _organizationSponsorshipRepository
.GetBySponsoredOrganizationIdAsync(sponsoredOrganizationId);
if (existingSponsorship == null)
{
await RemoveSponsorshipAsync(sponsoredOrganization);
// TODO on fail, mark org as disabled.
return false;
}
var validated = true;
if (existingSponsorship.SponsoringOrganizationId == null || existingSponsorship.SponsoringOrganizationUserId == null)
{
await RemoveSponsorshipAsync(sponsoredOrganization);
validated = false;
}
var sponsoringOrganization = await _organizationRepository
.GetByIdAsync(existingSponsorship.SponsoringOrganizationId.Value);
if (!sponsoringOrganization.Enabled)
{
await RemoveSponsorshipAsync(sponsoredOrganization);
validated = false;
}
if (!validated && existingSponsorship.SponsoredOrganizationId != null)
{
existingSponsorship.TimesRenewedWithoutValidation += 1;
existingSponsorship.SponsorshipLapsedDate ??= DateTime.UtcNow;
await _organizationSponsorshipRepository.UpsertAsync(existingSponsorship);
if (existingSponsorship.TimesRenewedWithoutValidation >= 6)
{
sponsoredOrganization.Enabled = false;
await _organizationRepository.UpsertAsync(sponsoredOrganization);
}
}
return true;
}
public async Task RemoveSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship = null)
{
var success = await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization);
await _organizationRepository.UpsertAsync(sponsoredOrganization);
if (sponsorship == null)
{
return;
}
if (success)
{
// 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);
}
}
else
{
sponsorship.SponsoringOrganizationId = null;
sponsorship.SponsoringOrganizationUserId = null;
if (!sponsorship.CloudSponsor)
{
// Sef-hosted sponsorship record
// we need to make the existing sponsorship available, and add
// a new sponsorship record to record the lapsed sponsorship
var cleanSponsorship = new OrganizationSponsorship
{
InstallationId = sponsorship.InstallationId,
SponsoringOrganizationId = sponsorship.SponsoringOrganizationId,
SponsoringOrganizationUserId = sponsorship.SponsoringOrganizationUserId,
CloudSponsor = sponsorship.CloudSponsor,
};
await _organizationSponsorshipRepository.UpsertAsync(cleanSponsorship);
}
sponsorship.SponsorshipLapsedDate ??= DateTime.UtcNow;
await _organizationSponsorshipRepository.UpsertAsync(sponsorship);
}
}
}

View File

@ -192,6 +192,44 @@ namespace Bit.Core.Services
}
}
public async Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship)
{
var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId);
var sub = await _stripeAdapter.SubscriptionGetAsync(org.GatewaySubscriptionId);
var sponsoredSubscription = new SponsoredOrganizationSubscription(org, sub);
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
false, PaymentMethodType.None, sponsoredSubscription.GetSponsorSubscriptionOptions(sponsorship), null);
org.GatewaySubscriptionId = subscription.Id;
org.ExpirationDate = subscription.CurrentPeriodEnd;
}
public async Task<bool> RemoveOrganizationSponsorshipAsync(Organization org)
{
var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId);
var sub = await _stripeAdapter.SubscriptionGetAsync(org.GatewaySubscriptionId);
var sponsoredSubscription = new SponsoredOrganizationSubscription(org, sub);
var subCreateOptions = sponsoredSubscription.RemoveOrganizationSubscriptionOptions();
var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions);
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
stripePaymentMethod, paymentMethodType, subCreateOptions, null);
if (subscription.Status == "incomplete")
{
// TODO: revert
return false;
}
org.GatewaySubscriptionId = subscription.Id;
org.Enabled = true;
org.ExpirationDate = subscription.CurrentPeriodEnd;
return true;
}
public async Task<string> UpgradeFreeOrganizationAsync(Organization org, StaticStore.Plan plan,
short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo)
{
@ -227,6 +265,29 @@ namespace Bit.Core.Services
}
var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon);
var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions);
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
stripePaymentMethod, paymentMethodType, subCreateOptions, null);
org.GatewaySubscriptionId = subscription.Id;
if (subscription.Status == "incomplete" &&
subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
{
org.Enabled = false;
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
}
else
{
org.Enabled = true;
org.ExpirationDate = subscription.CurrentPeriodEnd;
return null;
}
}
private (bool stripePaymentMethod, PaymentMethodType PaymentMethodType) IdentifyPaymentMethod(
Stripe.Customer customer, Stripe.SubscriptionCreateOptions subCreateOptions)
{
var stripePaymentMethod = false;
var paymentMethodType = PaymentMethodType.Credit;
var hasBtCustomerId = customer.Metadata.ContainsKey("btCustomerId");
@ -265,23 +326,7 @@ namespace Bit.Core.Services
}
}
}
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
stripePaymentMethod, paymentMethodType, subCreateOptions, null);
org.GatewaySubscriptionId = subscription.Id;
if (subscription.Status == "incomplete" &&
subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
{
org.Enabled = false;
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
}
else
{
org.Enabled = true;
org.ExpirationDate = subscription.CurrentPeriodEnd;
return null;
}
return (stripePaymentMethod, paymentMethodType);
}
public async Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType,

View File

@ -1,6 +1,8 @@
using Bit.Core.Enums;
using Bit.Core.Models.StaticStore;
using Bit.Core.Models.Table;
using System.Collections.Generic;
using System.Linq;
namespace Bit.Core.Utilities
{
@ -475,5 +477,19 @@ namespace Bit.Core.Utilities
public static IDictionary<GlobalEquivalentDomainsType, IEnumerable<string>> GlobalDomains { get; set; }
public static IEnumerable<Plan> Plans { get; set; }
public static IEnumerable<SponsoredPlan> SponsoredPlans { get; set; } = new[]
{
new SponsoredPlan
{
PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise,
SponsoredProductType = ProductType.Families,
SponsoringProductType = ProductType.Enterprise,
StripePlanId = "2021-enterprise-sponsored-families-org-monthly"
}
};
public static Plan GetPlan(PlanType planType) =>
Plans.FirstOrDefault(p => p.Type == planType);
public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) =>
SponsoredPlans.FirstOrDefault(p => p.PlanSponsorshipType == planSponsorshipType);
}
}

View File

@ -0,0 +1,31 @@
CREATE PROCEDURE [dbo].[OrganizationSponsorship_OrganizationDeleted]
@OrganizationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
UPDATE
[dbo].[OrganizationSponsorship]
SET
[SponsoringOrganizationId] = NULL
WHERE
[SponsoringOrganizationId] = @OrganizationId AND
[CloudSponsor] = 0
UPDATE
[dbo].[OrganizationSponsorship]
SET
[SponsoredOrganizationId] = NULL
WHERE
[SponsoredOrganizationId] = @OrganizationId AND
[CloudSponsor] = 0
DELETE
FROM
[dbo].[OrganizationSponsorship]
WHERE
[CloudSponsor] = 1 AND
([SponsoredOrganizationId] = @OrganizationId OR
[SponsoringOrganizationId] = @OrganizationId)
END
GO

View File

@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[OrganizationSponsorship_OrganizationUserDeleted]
@OrganizationUserId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
DELETE
FROM
[dbo].[OrganizationSponsorship]
WHERE
[SponsoringOrganizationUserId] = @OrganizationUserId
END
GO

View File

@ -0,0 +1,24 @@
CREATE PROCEDURE [dbo].[OrganizationSponsorship_OrganizationUsersDeleted]
@SponsoringOrganizationUserIds [dbo].[GuidIdArray] READONLY
AS
BEGIN
SET NOCOUNT ON
SET @BatchSize = 100;
WHILE @BatchSize > 0
BEGIN
BEGIN TRANSACTION OrganizationSponsorship_DeleteOUs
DELETE TOP(@BatchSize) OS
FROM
[dbo].[OrganiozationSponsorship] OS
INNER JOIN
@Ids I ON I.Id = OS.SponsoringOrganizationUserId
SET @BatchSize = @@ROWCOUNT
COMMIT TRANSACTION OrganizationSponsorship_DeleteOUs
END
END
GO

View File

@ -34,9 +34,19 @@ BEGIN
WHERE
[OrganizationUserId] = @Id
EXEC [dbo].[OrganizationUser_DeleteById] @Id
DELETE
FROM
[dbo].[OrganizationUser]
WHERE
[Id] = @Id
END
END
GO
IF OBJECT_ID('[dbo].[OrganizationUser_DeleteByIds]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[OrganizationUser_DeleteByIds]
END
GO

View File

@ -61,6 +61,7 @@ BEGIN
COMMIT TRANSACTION GoupUser_DeleteMany_GroupUsers
END
EXEC [dbo].[OrganizationSponsorship_OrganizationUsersDeleted] @Ids
SET @BatchSize = 100;

View File

@ -57,6 +57,8 @@ BEGIN
WHERE
[OrganizationId] = @Id
EXEC[dbo].[OrganizationSponsorship_OrganizationDeleted] @Id
DELETE
FROM
[dbo].[Organization]
@ -65,3 +67,4 @@ BEGIN
COMMIT TRANSACTION Organization_DeleteById
END
GO

View File

@ -1,8 +1,8 @@
CREATE TABLE [dbo].[OrganizationSponsorship] (
[Id] UNIQUEIDENTIFIER NOT NULL,
[InstallationId] UNIQUEIDENTIFIER NULL,
[SponsoringOrganizationId] UNIQUEIDENTIFIER NOT NULL,
[SponsoringOrganizationUserID] UNIQUEIDENTIFIER NOT NULL,
[SponsoringOrganizationId] UNIQUEIDENTIFIER NULL,
[SponsoringOrganizationUserID] UNIQUEIDENTIFIER NULL,
[SponsoredOrganizationId] UNIQUEIDENTIFIER NULL,
[OfferedToEmail] NVARCHAR (256) NULL,
[PlanSponsorshipType] TINYINT NULL,
@ -19,24 +19,25 @@ CREATE TABLE [dbo].[OrganizationSponsorship] (
GO
CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_InstallationId]
ON [dbo].[Organization]([Id] ASC, [InstallationId] ASC)
ON [dbo].[OrganizationSponsorship]([InstallationId] ASC)
WHERE [InstallationId] IS NOT NULL;
GO
CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationId]
ON [dbo].[Organization]([Id] ASC, [SponsoringOrganizationId] ASC)
ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationId] ASC)
WHERE [SponsoringOrganizationId] IS NOT NULL;
GO
CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationUserId]
ON [dbo].[Organization]([Id] ASC, [SponsorginOrganizationUserID] ASC)
ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationUserID] ASC)
WHERE [SponsoringOrganizationUserID] IS NOT NULL;
GO
CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_OfferedToEmail]
ON [dbo].[Organization]([Id] ASC, [OfferedToEmail] ASC)
ON [dbo].[OrganizationSponsorship]([OfferedToEmail] ASC)
WHERE [OfferedToEmail] IS NOT NULL;
GO
CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoredOrganizationID]
ON [dbo].[Organization]([Id] ASC, [SponsoredOrganizationId] ASC)
ON [dbo].[OrganizationSponsorship]([SponsoredOrganizationId] ASC)
WHERE [SponsoredOrganizationId] IS NOT NULL;

View File

@ -16,6 +16,7 @@ using Bit.Core.Repositories;
using Bit.Core.Models.Api.Request;
using Bit.Core.Services;
using Bit.Core.Models.Api;
using Bit.Core.Utilities;
namespace Bit.Api.Test.Controllers
{
@ -24,27 +25,29 @@ namespace Bit.Api.Test.Controllers
public class OrganizationSponsorshipsControllerTests
{
public static IEnumerable<object[]> EnterprisePlanTypes =>
Enum.GetValues<PlanType>().Where(p => PlanTypeHelper.IsEnterprise(p)).Select(p => new object[] { p });
Enum.GetValues<PlanType>().Where(p => StaticStore.GetPlan(p).Product == ProductType.Enterprise).Select(p => new object[] { p });
public static IEnumerable<object[]> NonEnterprisePlanTypes =>
Enum.GetValues<PlanType>().Where(p => !PlanTypeHelper.IsEnterprise(p)).Select(p => new object[] { p });
Enum.GetValues<PlanType>().Where(p => StaticStore.GetPlan(p).Product != ProductType.Enterprise).Select(p => new object[] { p });
public static IEnumerable<object[]> NonFamiliesPlanTypes =>
Enum.GetValues<PlanType>().Where(p => !PlanTypeHelper.IsFamilies(p)).Select(p => new object[] { p });
Enum.GetValues<PlanType>().Where(p => StaticStore.GetPlan(p).Product != ProductType.Families).Select(p => new object[] { p });
[Theory]
[BitMemberAutoData(nameof(NonEnterprisePlanTypes))]
public async Task CreateSponsorship_BadSponsoringOrgPlan_ThrowsBadRequest(PlanType sponsoringOrgPlan, Organization org,
SutProvider<OrganizationSponsorshipsController> sutProvider)
OrganizationSponsorshipRequestModel model, SutProvider<OrganizationSponsorshipsController> sutProvider)
{
org.PlanType = sponsoringOrgPlan;
model.PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CreateSponsorship(org.Id.ToString(), null));
sutProvider.Sut.CreateSponsorship(org.Id.ToString(), model));
Assert.Contains("Specified Organization cannot sponsor other organizations.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipService>()
.DidNotReceiveWithAnyArgs()
.OfferSponsorshipAsync(default, default, default, default);
.OfferSponsorshipAsync(default, default, default, default, default);
}
public static IEnumerable<object[]> NonConfirmedOrganizationUsersStatuses =>
@ -73,7 +76,7 @@ namespace Bit.Api.Test.Controllers
Assert.Contains("Only confirm users can sponsor other organizations.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipService>()
.DidNotReceiveWithAnyArgs()
.OfferSponsorshipAsync(default, default, default, default);
.OfferSponsorshipAsync(default, default, default, default, default);
}
[Theory]
@ -96,7 +99,7 @@ namespace Bit.Api.Test.Controllers
Assert.Contains("Can only create organization sponsorships for yourself.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipService>()
.DidNotReceiveWithAnyArgs()
.OfferSponsorshipAsync(default, default, default, default);
.OfferSponsorshipAsync(default, default, default, default, default);
}
[Theory]
@ -121,7 +124,7 @@ namespace Bit.Api.Test.Controllers
Assert.Contains("Can only sponsor one organization per Organization User.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipService>()
.DidNotReceiveWithAnyArgs()
.OfferSponsorshipAsync(default, default, default, default);
.OfferSponsorshipAsync(default, default, default, default, default);
}
[Theory]
@ -272,7 +275,7 @@ namespace Bit.Api.Test.Controllers
Assert.Contains("Can only revoke a sponsorship you granted.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipService>()
.DidNotReceiveWithAnyArgs()
.RemoveSponsorshipAsync(default);
.RemoveSponsorshipAsync(default, default);
}
[Theory]
@ -293,10 +296,58 @@ namespace Bit.Api.Test.Controllers
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RevokeSponsorship(sponsoringOrgUser.Id.ToString()));
Assert.Contains("You are not currently sponsoring and organization.", exception.Message);
Assert.Contains("You are not currently sponsoring an organization.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipService>()
.DidNotReceiveWithAnyArgs()
.RemoveSponsorshipAsync(default);
.RemoveSponsorshipAsync(default, default);
}
[Theory]
[BitAutoData]
public async Task RevokeSponsorship_SponsorshipNotRedeemed_ThrowsBadRequest(OrganizationUser sponsoringOrgUser,
OrganizationSponsorship sponsorship, SutProvider<OrganizationSponsorshipsController> sutProvider)
{
sponsorship.SponsoredOrganizationId = null;
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(sponsoringOrgUser.UserId);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(sponsoringOrgUser.Id)
.Returns(sponsoringOrgUser);
sutProvider.GetDependency<IOrganizationSponsorshipRepository>()
.GetBySponsoringOrganizationUserIdAsync(Arg.Is<Guid>(v => v != sponsoringOrgUser.Id))
.Returns(sponsorship);
sutProvider.GetDependency<IOrganizationSponsorshipRepository>()
.GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id)
.Returns((OrganizationSponsorship)sponsorship);
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RevokeSponsorship(sponsoringOrgUser.Id.ToString()));
Assert.Contains("You are not currently sponsoring an organization.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipService>()
.DidNotReceiveWithAnyArgs()
.RemoveSponsorshipAsync(default, default);
}
[Theory]
[BitAutoData]
public async Task RevokeSponsorship_SponsoredOrgNotFound_ThrowsBadRequest(OrganizationUser sponsoringOrgUser,
OrganizationSponsorship sponsorship, SutProvider<OrganizationSponsorshipsController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(sponsoringOrgUser.UserId);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(sponsoringOrgUser.Id)
.Returns(sponsoringOrgUser);
sutProvider.GetDependency<IOrganizationSponsorshipRepository>()
.GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id)
.Returns(sponsorship);
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RevokeSponsorship(sponsoringOrgUser.Id.ToString()));
Assert.Contains("Unable to find the sponsored Organization.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipService>()
.DidNotReceiveWithAnyArgs()
.RemoveSponsorshipAsync(default, default);
}
[Theory]
@ -312,7 +363,7 @@ namespace Bit.Api.Test.Controllers
Assert.Contains("Only the owner of an organization can remove sponsorship.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipService>()
.DidNotReceiveWithAnyArgs()
.RemoveSponsorshipAsync(default);
.RemoveSponsorshipAsync(default, default);
}
[Theory]
@ -334,7 +385,26 @@ namespace Bit.Api.Test.Controllers
Assert.Contains("The requested organization is not currently being sponsored.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipService>()
.DidNotReceiveWithAnyArgs()
.RemoveSponsorshipAsync(default);
.RemoveSponsorshipAsync(default, default);
}
[Theory]
[BitAutoData]
public async Task RemoveSponsorship_SponsoredOrgNotFound_ThrowsBadRequest(Organization sponsoredOrg,
OrganizationSponsorship sponsorship, SutProvider<OrganizationSponsorshipsController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(Arg.Any<Guid>()).Returns(true);
sutProvider.GetDependency<IOrganizationSponsorshipRepository>()
.GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id)
.Returns(sponsorship);
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RemoveSponsorship(sponsoredOrg.Id.ToString()));
Assert.Contains("Unable to find the sponsored Organization.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipService>()
.DidNotReceiveWithAnyArgs()
.RemoveSponsorshipAsync(default, default);
}
}
}

View File

@ -1,39 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Bit.Core.Enums;
using Bit.Core.Models.Table;
using Xunit;
namespace Bit.Core.Test.Enums
{
public class PlanTypeHelperTests
{
private static IEnumerable<PlanType> PlanArchetypeArray(PlanType planType) => new PlanType?[] {
PlanTypeHelper.HasFreePlan(new Organization {PlanType = planType}) ? planType : null,
PlanTypeHelper.HasFamiliesPlan(new Organization {PlanType = planType}) ? planType : null,
PlanTypeHelper.HasTeamsPlan(new Organization {PlanType = planType}) ? planType : null,
PlanTypeHelper.HasEnterprisePlan(new Organization {PlanType = planType}) ? planType : null,
}.Where(v => v.HasValue).Select(v => (PlanType)v);
public static IEnumerable<object[]> PlanTypes => Enum.GetValues<PlanType>().Select(p => new object[] { p });
public static IEnumerable<object[]> PlanTypesExceptCustom =>
Enum.GetValues<PlanType>().Except(new[] { PlanType.Custom }).Select(p => new object[] { p });
[Theory]
[MemberData(nameof(PlanTypesExceptCustom))]
public void NonCustomPlanTypesBelongToPlanArchetype(PlanType planType)
{
Assert.Contains(planType, PlanArchetypeArray(planType));
}
[Theory]
[MemberData(nameof(PlanTypesExceptCustom))]
public void PlanTypesBelongToOnlyOneArchetype(PlanType planType)
{
Console.WriteLine(planType);
Assert.Single(PlanArchetypeArray(planType));
}
}
}

View File

@ -34,15 +34,16 @@ namespace Bit.Core.Test.Services
[Theory]
[BitAutoData]
public async Task OfferSponsorship_CreatesSponsorship(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser,
string sponsoredEmail, SutProvider<OrganizationSponsorshipService> sutProvider)
string sponsoredEmail, string friendlyName, SutProvider<OrganizationSponsorshipService> sutProvider)
{
await sutProvider.Sut.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail);
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName);
var expectedSponsorship = new OrganizationSponsorship
{
SponsoringOrganizationId = sponsoringOrg.Id,
SponsoringOrganizationUserId = sponsoringOrgUser.Id,
FriendlyName = friendlyName,
OfferedToEmail = sponsoredEmail,
CloudSponsor = true,
};
@ -55,7 +56,7 @@ namespace Bit.Core.Test.Services
[Theory]
[BitAutoData]
public async Task OfferSponsorship_CreateSponsorshipThrows_RevertsDatabase(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser,
string sponsoredEmail, SutProvider<OrganizationSponsorshipService> sutProvider)
string sponsoredEmail, string friendlyName, SutProvider<OrganizationSponsorshipService> sutProvider)
{
var expectedException = new Exception();
OrganizationSponsorship createdSponsorship = null;
@ -68,7 +69,7 @@ namespace Bit.Core.Test.Services
var actualException = await Assert.ThrowsAsync<Exception>(() =>
sutProvider.Sut.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail));
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName));
Assert.Same(expectedException, actualException);
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)

View File

@ -4,9 +4,10 @@ BEGIN
CREATE TABLE [dbo].[OrganizationSponsorship] (
[Id] UNIQUEIDENTIFIER NOT NULL,
[InstallationId] UNIQUEIDENTIFIER NULL,
[SponsoringOrganizationId] UNIQUEIDENTIFIER NOT NULL,
[SponsoringOrganizationUserID] UNIQUEIDENTIFIER NOT NULL,
[SponsoringOrganizationId] UNIQUEIDENTIFIER NULL,
[SponsoringOrganizationUserID] UNIQUEIDENTIFIER NULL,
[SponsoredOrganizationId] UNIQUEIDENTIFIER NULL,
[FriendlyName] NVARCHAR(256) NULL,
[OfferedToEmail] NVARCHAR (256) NULL,
[PlanSponsorshipType] TINYINT NULL,
[CloudSponsor] BIT NULL,
@ -35,6 +36,7 @@ IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationSponsors
BEGIN
CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationId]
ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationId] ASC)
WHERE [SponsoringOrganizationId] IS NOT NULL;
END
GO
@ -42,6 +44,7 @@ IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationSponsors
BEGIN
CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationUserId]
ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationUserID] ASC)
WHERE [SponsoringOrganizationUserID] IS NOT NULL;
END
GO
@ -114,6 +117,7 @@ CREATE PROCEDURE [dbo].[OrganizationSponsorship_Create]
@SponsoringOrganizationId UNIQUEIDENTIFIER,
@SponsoringOrganizationUserID UNIQUEIDENTIFIER,
@SponsoredOrganizationId UNIQUEIDENTIFIER,
@FriendlyName NVARCHAR(256),
@OfferedToEmail NVARCHAR(256),
@PlanSponsorshipType TINYINT,
@CloudSponsor BIT,
@ -131,6 +135,7 @@ BEGIN
[SponsoringOrganizationId],
[SponsoringOrganizationUserID],
[SponsoredOrganizationId],
[FriendlyName],
[OfferedToEmail],
[PlanSponsorshipType],
[CloudSponsor],
@ -145,6 +150,7 @@ BEGIN
@SponsoringOrganizationId,
@SponsoringOrganizationUserID,
@SponsoredOrganizationId,
@FriendlyName,
@OfferedToEmail,
@PlanSponsorshipType,
@CloudSponsor,
@ -168,6 +174,7 @@ CREATE PROCEDURE [dbo].[OrganizationSponsorship_Update]
@SponsoringOrganizationId UNIQUEIDENTIFIER,
@SponsoringOrganizationUserID UNIQUEIDENTIFIER,
@SponsoredOrganizationId UNIQUEIDENTIFIER,
@FriendlyName NVARCHAR(256),
@OfferedToEmail NVARCHAR(256),
@PlanSponsorshipType TINYINT,
@CloudSponsor BIT,
@ -185,6 +192,7 @@ BEGIN
[SponsoringOrganizationId] = @SponsoringOrganizationId,
[SponsoringOrganizationUserID] = @SponsoringOrganizationUserID,
[SponsoredOrganizationId] = @SponsoredOrganizationId,
[FriendlyName] = @FriendlyName,
[OfferedToEmail] = @OfferedToEmail,
[PlanSponsorshipType] = @PlanSponsorshipType,
[CloudSponsor] = @CloudSponsor,
@ -290,3 +298,365 @@ BEGIN
[OfferedToEmail] = @OfferedToEmail
END
GO
-- OrganizationSponsorship_OrganizationDeleted
IF OBJECT_ID('[dbo].[OrganizationSponsorship_OrganizationDeleted]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[OrganizationSponsorship_OrganizationDeleted]
END
GO
CREATE PROCEDURE [dbo].[OrganizationSponsorship_OrganizationDeleted]
@OrganizationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
UPDATE
[dbo].[OrganizationSponsorship]
SET
[SponsoringOrganizationId] = NULL
WHERE
[SponsoringOrganizationId] = @OrganizationId AND
[CloudSponsor] = 0
UPDATE
[dbo].[OrganizationSponsorship]
SET
[SponsoredOrganizationId] = NULL
WHERE
[SponsoredOrganizationId] = @OrganizationId AND
[CloudSponsor] = 0
DELETE
FROM
[dbo].[OrganizationSponsorship]
WHERE
[CloudSponsor] = 1 AND
([SponsoredOrganizationId] = @OrganizationId OR
[SponsoringOrganizationId] = @OrganizationId)
END
GO
-- OrganizationSponsorship_OrganizationUserDeleted
IF OBJECT_ID('[dbo].[OrganizationSponsorship_OrganizationUserDeleted]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[OrganizationSponsorship_OrganizationUserDeleted]
END
GO
CREATE PROCEDURE [dbo].[OrganizationSponsorship_OrganizationUserDeleted]
@OrganizationUserId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
DELETE
FROM
[dbo].[OrganizationSponsorship]
WHERE
[SponsoringOrganizationUserId] = @OrganizationUserId
END
GO
-- OrganizationSponsorship_OrganizationUsersDeleted
IF OBJECT_ID('[dbo].[OrganizationSponsorship_OrganizationUsersDeleted]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[OrganizationSponsorship_OrganizationUsersDeleted]
END
GO
CREATE PROCEDURE [dbo].[OrganizationSponsorship_OrganizationUsersDeleted]
@SponsoringOrganizationUserIds [dbo].[GuidIdArray] READONLY
AS
BEGIN
SET NOCOUNT ON
DECLARE @BatchSize INT = 100
WHILE @BatchSize > 0
BEGIN
BEGIN TRANSACTION OS_DeleteMany_OUs
DELETE TOP(@BatchSize) OS
FROM
[dbo].[OrganizationSponsorship] OS
INNER JOIN
@SponsoringOrganizationUserIds I ON I.Id = OS.SponsoringOrganizationUserId
SET @BatchSize = @@ROWCOUNT
COMMIT TRANSACTION OS_DeleteMany_OUs
END
END
GO
-- Update Organization delete sprocs to handle organization sponsorships
IF OBJECT_ID('[dbo].[Organization_DeleteById]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[Organization_DeleteById]
END
GO
CREATE PROCEDURE [dbo].[Organization_DeleteById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @Id
DECLARE @BatchSize INT = 100
WHILE @BatchSize > 0
BEGIN
BEGIN TRANSACTION Organization_DeleteById_Ciphers
DELETE TOP(@BatchSize)
FROM
[dbo].[Cipher]
WHERE
[UserId] IS NULL
AND [OrganizationId] = @Id
SET @BatchSize = @@ROWCOUNT
COMMIT TRANSACTION Organization_DeleteById_Ciphers
END
BEGIN TRANSACTION Organization_DeleteById
DELETE
FROM
[dbo].[SsoUser]
WHERE
[OrganizationId] = @Id
DELETE
FROM
[dbo].[SsoConfig]
WHERE
[OrganizationId] = @Id
DELETE CU
FROM
[dbo].[CollectionUser] CU
INNER JOIN
[dbo].[OrganizationUser] OU ON [CU].[OrganizationUserId] = [OU].[Id]
WHERE
[OU].[OrganizationId] = @Id
DELETE
FROM
[dbo].[OrganizationUser]
WHERE
[OrganizationId] = @Id
DELETE
FROM
[dbo].[ProviderOrganization]
WHERE
[OrganizationId] = @Id
EXEC[dbo].[OrganizationSponsorship_OrganizationDeleted] @Id
DELETE
FROM
[dbo].[Organization]
WHERE
[Id] = @Id
COMMIT TRANSACTION Organization_DeleteById
END
GO
-- Update Organization User delete sprocs to handle organization sponsorships
IF OBJECT_ID('[dbo].[OrganizationUser_DeleteById]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[OrganizationUser_DeleteById]
END
GO
CREATE PROCEDURE [dbo].[OrganizationUser_DeleteById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserId] @Id
DECLARE @OrganizationId UNIQUEIDENTIFIER
DECLARE @UserId UNIQUEIDENTIFIER
SELECT
@OrganizationId = [OrganizationId],
@UserId = [UserId]
FROM
[dbo].[OrganizationUser]
WHERE
[Id] = @Id
IF @OrganizationId IS NOT NULL AND @UserId IS NOT NULL
BEGIN
EXEC [dbo].[SsoUser_Delete] @UserId, @OrganizationId
END
DELETE
FROM
[dbo].[CollectionUser]
WHERE
[OrganizationUserId] = @Id
DELETE
FROM
[dbo].[GroupUser]
WHERE
[OrganizationUserId] = @Id
EXEC [dbo].[OrganizationUser_DeleteById] @Id
DELETE
FROM
[dbo].[OrganizationUser]
WHERE
[Id] = @Id
END
GO
IF OBJECT_ID('[dbo].[OrganizationUser_DeleteByIds]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[OrganizationUser_DeleteByIds]
END
GO
CREATE PROCEDURE [dbo].[OrganizationUser_DeleteByIds]
@Ids [dbo].[GuidIdArray] READONLY
AS
BEGIN
SET NOCOUNT ON
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIds] @Ids
DECLARE @UserAndOrganizationIds [dbo].[TwoGuidIdArray]
INSERT INTO @UserAndOrganizationIds
(Id1, Id2)
SELECT
UserId,
OrganizationId
FROM
[dbo].[OrganizationUser] OU
INNER JOIN
@Ids OUIds ON OUIds.Id = OU.Id
WHERE
UserId IS NOT NULL AND
OrganizationId IS NOT NULL
BEGIN
EXEC [dbo].[SsoUser_DeleteMany] @UserAndOrganizationIds
END
DECLARE @BatchSize INT = 100
-- Delete CollectionUsers
WHILE @BatchSize > 0
BEGIN
BEGIN TRANSACTION CollectionUser_DeleteMany_CUs
DELETE TOP(@BatchSize) CU
FROM
[dbo].[CollectionUser] CU
INNER JOIN
@Ids I ON I.Id = CU.OrganizationUserId
SET @BatchSize = @@ROWCOUNT
COMMIT TRANSACTION CollectionUser_DeleteMany_CUs
END
SET @BatchSize = 100;
-- Delete GroupUsers
WHILE @BatchSize > 0
BEGIN
BEGIN TRANSACTION GroupUser_DeleteMany_GroupUsers
DELETE TOP(@BatchSize) GU
FROM
[dbo].[GroupUser] GU
INNER JOIN
@Ids I ON I.Id = GU.OrganizationUserId
SET @BatchSize = @@ROWCOUNT
COMMIT TRANSACTION GoupUser_DeleteMany_GroupUsers
END
EXEC [dbo].[OrganizationSponsorship_OrganizationUsersDeleted] @Ids
SET @BatchSize = 100;
-- Delete OrganizationUsers
WHILE @BatchSize > 0
BEGIN
BEGIN TRANSACTION OrganizationUser_DeleteMany_OUs
DELETE TOP(@BatchSize) OU
FROM
[dbo].[OrganizationUser] OU
INNER JOIN
@Ids I ON I.Id = OU.Id
SET @BatchSize = @@ROWCOUNT
COMMIT TRANSACTION OrganizationUser_DeleteMany_OUs
END
END
GO
-- OrganizationUserOrganizationDetailsView update
ALTER VIEW [dbo].[OrganizationUserOrganizationDetailsView]
AS
SELECT
OU.[UserId],
OU.[OrganizationId],
O.[Name],
O.[Enabled],
O.[UsePolicies],
O.[UseSso],
O.[UseGroups],
O.[UseDirectory],
O.[UseEvents],
O.[UseTotp],
O.[Use2fa],
O.[UseApi],
O.[UseResetPassword],
O.[SelfHost],
O.[UsersGetPremium],
O.[Seats],
O.[MaxCollections],
O.[MaxStorageGb],
O.[Identifier],
OU.[Key],
OU.[ResetPasswordKey],
O.[PublicKey],
O.[PrivateKey],
OU.[Status],
OU.[Type],
SU.[ExternalId] SsoExternalId,
OU.[Permissions],
PO.[ProviderId],
P.[Name] ProviderName,
OS.[FriendlyName] FamilySponsorshipFriendlyName
FROM
[dbo].[OrganizationUser] OU
INNER JOIN
[dbo].[Organization] O ON O.[Id] = OU.[OrganizationId]
LEFT JOIN
[dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId]
LEFT JOIN
[dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id]
LEFT JOIN
[dbo].[Provider] P ON P.[Id] = PO.[ProviderId]
LEFT JOIN
[dbo].[OrganizationSponsorship] OS ON OS.[SponsoringOrganizationUserId] = OU.[Id]

View File

@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Bit.MySqlMigrations.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20211104164838_OrganizationSponsorship")]
[Migration("20211108225243_OrganizationSponsorship")]
partial class OrganizationSponsorship
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -595,6 +595,10 @@ namespace Bit.MySqlMigrations.Migrations
b.Property<bool>("CloudSponsor")
.HasColumnType("tinyint(1)");
b.Property<string>("FriendlyName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<Guid?>("InstallationId")
.HasColumnType("char(36)");
@ -611,10 +615,10 @@ namespace Bit.MySqlMigrations.Migrations
b.Property<Guid?>("SponsoredOrganizationId")
.HasColumnType("char(36)");
b.Property<Guid>("SponsoringOrganizationId")
b.Property<Guid?>("SponsoringOrganizationId")
.HasColumnType("char(36)");
b.Property<Guid>("SponsoringOrganizationUserId")
b.Property<Guid?>("SponsoringOrganizationUserId")
.HasColumnType("char(36)");
b.Property<DateTime?>("SponsorshipLapsedDate")
@ -1356,9 +1360,7 @@ namespace Bit.MySqlMigrations.Migrations
b.HasOne("Bit.Core.Models.EntityFramework.Organization", "SponsoringOrganization")
.WithMany()
.HasForeignKey("SponsoringOrganizationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
.HasForeignKey("SponsoringOrganizationId");
b.Navigation("Installation");

View File

@ -20,9 +20,11 @@ namespace Bit.MySqlMigrations.Migrations
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
InstallationId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
SponsoringOrganizationId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
SponsoringOrganizationUserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
SponsoringOrganizationId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
SponsoringOrganizationUserId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
SponsoredOrganizationId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
FriendlyName = table.Column<string>(type: "varchar(256)", maxLength: 256, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
OfferedToEmail = table.Column<string>(type: "varchar(256)", maxLength: 256, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
PlanSponsorshipType = table.Column<byte>(type: "tinyint unsigned", nullable: true),
@ -51,7 +53,7 @@ namespace Bit.MySqlMigrations.Migrations
column: x => x.SponsoringOrganizationId,
principalTable: "Organization",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySql:CharSet", "utf8mb4");

View File

@ -593,6 +593,10 @@ namespace Bit.MySqlMigrations.Migrations
b.Property<bool>("CloudSponsor")
.HasColumnType("tinyint(1)");
b.Property<string>("FriendlyName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<Guid?>("InstallationId")
.HasColumnType("char(36)");
@ -609,10 +613,10 @@ namespace Bit.MySqlMigrations.Migrations
b.Property<Guid?>("SponsoredOrganizationId")
.HasColumnType("char(36)");
b.Property<Guid>("SponsoringOrganizationId")
b.Property<Guid?>("SponsoringOrganizationId")
.HasColumnType("char(36)");
b.Property<Guid>("SponsoringOrganizationUserId")
b.Property<Guid?>("SponsoringOrganizationUserId")
.HasColumnType("char(36)");
b.Property<DateTime?>("SponsorshipLapsedDate")
@ -1354,9 +1358,7 @@ namespace Bit.MySqlMigrations.Migrations
b.HasOne("Bit.Core.Models.EntityFramework.Organization", "SponsoringOrganization")
.WithMany()
.HasForeignKey("SponsoringOrganizationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
.HasForeignKey("SponsoringOrganizationId");
b.Navigation("Installation");

View File

@ -5,9 +5,10 @@ ALTER TABLE `User` ADD `UsesCryptoAgent` tinyint(1) NOT NULL DEFAULT FALSE;
CREATE TABLE `OrganizationSponsorship` (
`Id` char(36) COLLATE ascii_general_ci NOT NULL,
`InstallationId` char(36) COLLATE ascii_general_ci NULL,
`SponsoringOrganizationId` char(36) COLLATE ascii_general_ci NOT NULL,
`SponsoringOrganizationUserId` char(36) COLLATE ascii_general_ci NOT NULL,
`SponsoringOrganizationId` char(36) COLLATE ascii_general_ci NULL,
`SponsoringOrganizationUserId` char(36) COLLATE ascii_general_ci NULL,
`SponsoredOrganizationId` char(36) COLLATE ascii_general_ci NULL,
`FriendlyName` varchar(256) CHARACTER SET utf8mb4 NULL,
`OfferedToEmail` varchar(256) CHARACTER SET utf8mb4 NULL,
`PlanSponsorshipType` tinyint unsigned NULL,
`CloudSponsor` tinyint(1) NOT NULL,
@ -17,7 +18,7 @@ CREATE TABLE `OrganizationSponsorship` (
CONSTRAINT `PK_OrganizationSponsorship` PRIMARY KEY (`Id`),
CONSTRAINT `FK_OrganizationSponsorship_Installation_InstallationId` FOREIGN KEY (`InstallationId`) REFERENCES `Installation` (`Id`) ON DELETE RESTRICT,
CONSTRAINT `FK_OrganizationSponsorship_Organization_SponsoredOrganizationId` FOREIGN KEY (`SponsoredOrganizationId`) REFERENCES `Organization` (`Id`) ON DELETE RESTRICT,
CONSTRAINT `FK_OrganizationSponsorship_Organization_SponsoringOrganizationId` FOREIGN KEY (`SponsoringOrganizationId`) REFERENCES `Organization` (`Id`) ON DELETE CASCADE
CONSTRAINT `FK_OrganizationSponsorship_Organization_SponsoringOrganizationId` FOREIGN KEY (`SponsoringOrganizationId`) REFERENCES `Organization` (`Id`) ON DELETE RESTRICT
) CHARACTER SET utf8mb4;
CREATE INDEX `IX_OrganizationSponsorship_InstallationId` ON `OrganizationSponsorship` (`InstallationId`);
@ -27,6 +28,6 @@ CREATE INDEX `IX_OrganizationSponsorship_SponsoredOrganizationId` ON `Organizati
CREATE INDEX `IX_OrganizationSponsorship_SponsoringOrganizationId` ON `OrganizationSponsorship` (`SponsoringOrganizationId`);
INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`)
VALUES ('20211104164838_OrganizationSponsorship', '5.0.9');
VALUES ('20211108225243_OrganizationSponsorship', '5.0.9');
COMMIT;

View File

@ -10,7 +10,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Bit.PostgresMigrations.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20211104164532_OrganizationSponsorship")]
[Migration("20211108225011_OrganizationSponsorship")]
partial class OrganizationSponsorship
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -599,6 +599,10 @@ namespace Bit.PostgresMigrations.Migrations
b.Property<bool>("CloudSponsor")
.HasColumnType("boolean");
b.Property<string>("FriendlyName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<Guid?>("InstallationId")
.HasColumnType("uuid");
@ -615,10 +619,10 @@ namespace Bit.PostgresMigrations.Migrations
b.Property<Guid?>("SponsoredOrganizationId")
.HasColumnType("uuid");
b.Property<Guid>("SponsoringOrganizationId")
b.Property<Guid?>("SponsoringOrganizationId")
.HasColumnType("uuid");
b.Property<Guid>("SponsoringOrganizationUserId")
b.Property<Guid?>("SponsoringOrganizationUserId")
.HasColumnType("uuid");
b.Property<DateTime?>("SponsorshipLapsedDate")
@ -1365,9 +1369,7 @@ namespace Bit.PostgresMigrations.Migrations
b.HasOne("Bit.Core.Models.EntityFramework.Organization", "SponsoringOrganization")
.WithMany()
.HasForeignKey("SponsoringOrganizationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
.HasForeignKey("SponsoringOrganizationId");
b.Navigation("Installation");

View File

@ -20,9 +20,10 @@ namespace Bit.PostgresMigrations.Migrations
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
InstallationId = table.Column<Guid>(type: "uuid", nullable: true),
SponsoringOrganizationId = table.Column<Guid>(type: "uuid", nullable: false),
SponsoringOrganizationUserId = table.Column<Guid>(type: "uuid", nullable: false),
SponsoringOrganizationId = table.Column<Guid>(type: "uuid", nullable: true),
SponsoringOrganizationUserId = table.Column<Guid>(type: "uuid", nullable: true),
SponsoredOrganizationId = table.Column<Guid>(type: "uuid", nullable: true),
FriendlyName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
OfferedToEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
PlanSponsorshipType = table.Column<byte>(type: "smallint", nullable: true),
CloudSponsor = table.Column<bool>(type: "boolean", nullable: false),
@ -50,7 +51,7 @@ namespace Bit.PostgresMigrations.Migrations
column: x => x.SponsoringOrganizationId,
principalTable: "Organization",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(

View File

@ -597,6 +597,10 @@ namespace Bit.PostgresMigrations.Migrations
b.Property<bool>("CloudSponsor")
.HasColumnType("boolean");
b.Property<string>("FriendlyName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<Guid?>("InstallationId")
.HasColumnType("uuid");
@ -613,10 +617,10 @@ namespace Bit.PostgresMigrations.Migrations
b.Property<Guid?>("SponsoredOrganizationId")
.HasColumnType("uuid");
b.Property<Guid>("SponsoringOrganizationId")
b.Property<Guid?>("SponsoringOrganizationId")
.HasColumnType("uuid");
b.Property<Guid>("SponsoringOrganizationUserId")
b.Property<Guid?>("SponsoringOrganizationUserId")
.HasColumnType("uuid");
b.Property<DateTime?>("SponsorshipLapsedDate")
@ -1363,9 +1367,7 @@ namespace Bit.PostgresMigrations.Migrations
b.HasOne("Bit.Core.Models.EntityFramework.Organization", "SponsoringOrganization")
.WithMany()
.HasForeignKey("SponsoringOrganizationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
.HasForeignKey("SponsoringOrganizationId");
b.Navigation("Installation");

View File

@ -5,9 +5,10 @@ ALTER TABLE "User" ADD "UsesCryptoAgent" boolean NOT NULL DEFAULT FALSE;
CREATE TABLE "OrganizationSponsorship" (
"Id" uuid NOT NULL,
"InstallationId" uuid NULL,
"SponsoringOrganizationId" uuid NOT NULL,
"SponsoringOrganizationUserId" uuid NOT NULL,
"SponsoringOrganizationId" uuid NULL,
"SponsoringOrganizationUserId" uuid NULL,
"SponsoredOrganizationId" uuid NULL,
"FriendlyName" character varying(256) NULL,
"OfferedToEmail" character varying(256) NULL,
"PlanSponsorshipType" smallint NULL,
"CloudSponsor" boolean NOT NULL,
@ -17,7 +18,7 @@ CREATE TABLE "OrganizationSponsorship" (
CONSTRAINT "PK_OrganizationSponsorship" PRIMARY KEY ("Id"),
CONSTRAINT "FK_OrganizationSponsorship_Installation_InstallationId" FOREIGN KEY ("InstallationId") REFERENCES "Installation" ("Id") ON DELETE RESTRICT,
CONSTRAINT "FK_OrganizationSponsorship_Organization_SponsoredOrganizationId" FOREIGN KEY ("SponsoredOrganizationId") REFERENCES "Organization" ("Id") ON DELETE RESTRICT,
CONSTRAINT "FK_OrganizationSponsorship_Organization_SponsoringOrganization~" FOREIGN KEY ("SponsoringOrganizationId") REFERENCES "Organization" ("Id") ON DELETE CASCADE
CONSTRAINT "FK_OrganizationSponsorship_Organization_SponsoringOrganization~" FOREIGN KEY ("SponsoringOrganizationId") REFERENCES "Organization" ("Id") ON DELETE RESTRICT
);
CREATE INDEX "IX_OrganizationSponsorship_InstallationId" ON "OrganizationSponsorship" ("InstallationId");
@ -27,6 +28,6 @@ CREATE INDEX "IX_OrganizationSponsorship_SponsoredOrganizationId" ON "Organizati
CREATE INDEX "IX_OrganizationSponsorship_SponsoringOrganizationId" ON "OrganizationSponsorship" ("SponsoringOrganizationId");
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20211104164532_OrganizationSponsorship', '5.0.9');
VALUES ('20211108225011_OrganizationSponsorship', '5.0.9');
COMMIT;