1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-01 16:12: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 // TODO: validate has right to sponsor, send sponsorship email
var sponsoringOrgIdGuid = new Guid(sponsoringOrgId); var sponsoringOrgIdGuid = new Guid(sponsoringOrgId);
var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(model.PlanSponsorshipType)?.SponsoringProductType;
var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgIdGuid); 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."); 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."); 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")] [HttpPost("sponsored/redeem/families-for-enterprise")]
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
public async Task RedeemSponsorship([FromQuery] string sponsorshipToken, [FromBody] OrganizationSponsorshipRedeemRequestModel model) public async Task RedeemSponsorship([FromQuery] string sponsorshipToken, [FromBody] OrganizationSponsorshipRedeemRequestModel model)
{ {
// TODO: parse out sponsorshipInfo
if (!await _organizationsSponsorshipService.ValidateRedemptionTokenAsync(sponsorshipToken)) if (!await _organizationsSponsorshipService.ValidateRedemptionTokenAsync(sponsorshipToken))
{ {
throw new BadRequestException("Failed to parse sponsorship token."); 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."); 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); var organizationToSponsor = await _organizationRepository.GetByIdAsync(model.SponsoredOrganizationId);
// TODO: only current families plan? if (requiredSponsoredProductType == null ||
if (organizationToSponsor == null || !PlanTypeHelper.HasFamiliesPlan(organizationToSponsor)) organizationToSponsor == null ||
StaticStore.GetPlan(organizationToSponsor.PlanType).Product != requiredSponsoredProductType.Value)
{ {
throw new BadRequestException("Can only redeem sponsorship offer on families organizations."); throw new BadRequestException("Can only redeem sponsorship offer on families organizations.");
} }
@ -124,12 +130,19 @@ namespace Bit.Api.Controllers
var existingOrgSponsorship = await _organizationSponsorshipRepository var existingOrgSponsorship = await _organizationSponsorshipRepository
.GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUserIdGuid); .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}")] [HttpDelete("sponsored/{sponsoredOrgId}")]
@ -146,12 +159,20 @@ namespace Bit.Api.Controllers
var existingOrgSponsorship = await _organizationSponsorshipRepository var existingOrgSponsorship = await _organizationSponsorshipRepository
.GetBySponsoredOrganizationIdAsync(sponsoredOrgIdGuid); .GetBySponsoredOrganizationIdAsync(sponsoredOrgIdGuid);
if (existingOrgSponsorship == null) if (existingOrgSponsorship == null || existingOrgSponsorship.SponsoredOrganizationId == null)
{ {
throw new BadRequestException("The requested organization is not currently being sponsored."); 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 BillingSettings _billingSettings;
private readonly IWebHostEnvironment _hostingEnvironment; private readonly IWebHostEnvironment _hostingEnvironment;
private readonly IOrganizationService _organizationService; private readonly IOrganizationService _organizationService;
private readonly IOrganizationSponsorshipService _organizationSponsorshipService;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly ITransactionRepository _transactionRepository; private readonly ITransactionRepository _transactionRepository;
private readonly IUserService _userService; private readonly IUserService _userService;
@ -45,6 +46,7 @@ namespace Bit.Billing.Controllers
IOptions<BillingSettings> billingSettings, IOptions<BillingSettings> billingSettings,
IWebHostEnvironment hostingEnvironment, IWebHostEnvironment hostingEnvironment,
IOrganizationService organizationService, IOrganizationService organizationService,
IOrganizationSponsorshipService organizationSponsorshipService,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
ITransactionRepository transactionRepository, ITransactionRepository transactionRepository,
IUserService userService, IUserService userService,
@ -58,6 +60,7 @@ namespace Bit.Billing.Controllers
_billingSettings = billingSettings?.Value; _billingSettings = billingSettings?.Value;
_hostingEnvironment = hostingEnvironment; _hostingEnvironment = hostingEnvironment;
_organizationService = organizationService; _organizationService = organizationService;
_organizationSponsorshipService = organizationSponsorshipService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_transactionRepository = transactionRepository; _transactionRepository = transactionRepository;
_userService = userService; _userService = userService;
@ -136,6 +139,16 @@ namespace Bit.Billing.Controllers
// org // org
if (ids.Item1.HasValue) 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, await _organizationService.UpdateExpirationDateAsync(ids.Item1.Value,
subscription.CurrentPeriodEnd); subscription.CurrentPeriodEnd);
} }
@ -783,5 +796,8 @@ namespace Bit.Billing.Controllers
} }
return subscription; 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, GoogleInApp = 7,
[Display(Name = "Check")] [Display(Name = "Check")]
Check = 8, 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 namespace Bit.Core.Enums
{ {

View File

@ -1,6 +1,4 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq;
using Bit.Core.Models.Table;
namespace Bit.Core.Enums namespace Bit.Core.Enums
{ {
@ -29,26 +27,6 @@ namespace Bit.Core.Enums
[Display(Name = "Enterprise (Monthly)")] [Display(Name = "Enterprise (Monthly)")]
EnterpriseMonthly = 10, EnterpriseMonthly = 10,
[Display(Name = "Enterprise (Annually)")] [Display(Name = "Enterprise (Annually)")]
EnterpriseAnnually= 11, 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);
} }
} }

View File

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

View File

@ -16,6 +16,9 @@ namespace Bit.Core.Models.Api.Request
[Required] [Required]
[StringLength(256)] [StringLength(256)]
[StrictEmailAddress] [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(); UserId = organization.UserId?.ToString();
ProviderId = organization.ProviderId?.ToString(); ProviderId = organization.ProviderId?.ToString();
ProviderName = organization.ProviderName; ProviderName = organization.ProviderName;
FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName;
} }
public string Id { get; set; } public string Id { get; set; }
@ -68,5 +69,6 @@ namespace Bit.Core.Models.Api
public bool HasPublicAndPrivateKeys { get; set; } public bool HasPublicAndPrivateKeys { get; set; }
public string ProviderId { get; set; } public string ProviderId { get; set; }
public string ProviderName { 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 Bit.Core.Models.Table;
using Stripe; using Stripe;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Bit.Core.Models.Business namespace Bit.Core.Models.Business
{ {
public class OrganizationSubscriptionOptionsBase : Stripe.SubscriptionCreateOptions 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>(); Items = new List<SubscriptionItemOptions>();
Metadata = new Dictionary<string, string> Metadata = new Dictionary<string, string>
@ -14,15 +16,6 @@ namespace Bit.Core.Models.Business
[org.GatewayIdField()] = org.Id.ToString() [org.GatewayIdField()] = org.Id.ToString()
}; };
if (plan.StripePlanId != null)
{
Items.Add(new SubscriptionItemOptions
{
Plan = plan.StripePlanId,
Quantity = 1
});
}
if (additionalSeats > 0 && plan.StripeSeatPlanId != null) if (additionalSeats > 0 && plan.StripeSeatPlanId != null)
{ {
Items.Add(new SubscriptionItemOptions Items.Add(new SubscriptionItemOptions
@ -49,15 +42,53 @@ namespace Bit.Core.Models.Business
Quantity = 1 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( public OrganizationPurchaseSubscriptionOptions(
Organization org, StaticStore.Plan plan, Organization org, StaticStore.Plan plan,
@ -70,7 +101,7 @@ namespace Bit.Core.Models.Business
} }
} }
public class OrganizationUpgradeSubscriptionOptions : OrganizationSubscriptionOptionsBase public class OrganizationUpgradeSubscriptionOptions : UnsponsoredOrganizationSubscriptionOptionsBase
{ {
public OrganizationUpgradeSubscriptionOptions( public OrganizationUpgradeSubscriptionOptions(
string customerId, Organization org, string customerId, Organization org,
@ -81,5 +112,43 @@ namespace Bit.Core.Models.Business
{ {
Customer = customerId; 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 string PrivateKey { get; set; }
public Guid? ProviderId { get; set; } public Guid? ProviderId { get; set; }
public string ProviderName { 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 Id { get; set; }
public Guid? InstallationId { get; set; } public Guid? InstallationId { get; set; }
[Required] public Guid? SponsoringOrganizationId { get; set; }
public Guid SponsoringOrganizationId { get; set; } public Guid? SponsoringOrganizationUserId { get; set; }
[Required]
public Guid SponsoringOrganizationUserId { get; set; }
public Guid? SponsoredOrganizationId { get; set; } public Guid? SponsoredOrganizationId { get; set; }
[MaxLength(256)] [MaxLength(256)]
public string FriendlyName { get; set; }
[MaxLength(256)]
public string OfferedToEmail { get; set; } public string OfferedToEmail { get; set; }
public PlanSponsorshipType? PlanSponsorshipType { get; set; } public PlanSponsorshipType? PlanSponsorshipType { get; set; }
[Required] [Required]

View File

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

View File

@ -95,5 +95,29 @@ namespace Bit.Core.Repositories.EntityFramework
{ {
await OrganizationUpdateStorage(id); 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 class OrganizationSponsorshipRepository : Repository<TableModel.OrganizationSponsorship, EFModel.OrganizationSponsorship, Guid>, IOrganizationSponsorshipRepository
{ {
public OrganizationSponsorshipRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : 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(); 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) public async Task DeleteManyAsync(IEnumerable<Guid> organizationUserIds)
{ {
using (var scope = ServiceScopeFactory.CreateScope()) using (var scope = ServiceScopeFactory.CreateScope())
{ {
var dbContext = GetDatabaseContext(scope); var dbContext = GetDatabaseContext(scope);
var entities = dbContext.FindAsync<EfModel.OrganizationUser>(organizationUserIds); 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); dbContext.RemoveRange(entities);
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
} }

View File

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

View File

@ -1,3 +1,4 @@
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
@ -7,8 +8,10 @@ namespace Bit.Core.Services
public interface IOrganizationSponsorshipService public interface IOrganizationSponsorshipService
{ {
Task<bool> ValidateRedemptionTokenAsync(string encryptedToken); 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 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 System.Threading.Tasks;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
using Bit.Core.Enums; using Bit.Core.Enums;
namespace Bit.Core.Services namespace Bit.Core.Services
@ -10,13 +11,15 @@ namespace Bit.Core.Services
{ {
Task CancelAndRecoverChargesAsync(ISubscriber subscriber); Task CancelAndRecoverChargesAsync(ISubscriber subscriber);
Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, 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); 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); short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo);
Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
short additionalStorageGb, TaxInfo taxInfo); 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<string> AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId, DateTime? prorationDate = null);
Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false, Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false,
bool skipInAppPurchaseCheck = false); bool skipInAppPurchaseCheck = false);

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
@ -13,12 +14,18 @@ namespace Bit.Core.Services
private const string TokenClearTextPrefix = "BWOrganizationSponsorship_"; private const string TokenClearTextPrefix = "BWOrganizationSponsorship_";
private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IPaymentService _paymentService;
private readonly IDataProtector _dataProtector; private readonly IDataProtector _dataProtector;
public OrganizationSponsorshipService(IOrganizationSponsorshipRepository organizationSponsorshipRepository, public OrganizationSponsorshipService(IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IOrganizationRepository organizationRepository,
IPaymentService paymentService,
IDataProtector dataProtector) IDataProtector dataProtector)
{ {
_organizationSponsorshipRepository = organizationSponsorshipRepository; _organizationSponsorshipRepository = organizationSponsorshipRepository;
_organizationRepository = organizationRepository;
_paymentService = paymentService;
_dataProtector = dataProtector; _dataProtector = dataProtector;
} }
@ -63,13 +70,16 @@ namespace Bit.Core.Services
_dataProtector.Protect($"{FamiliesForEnterpriseTokenName} {sponsorshipId} {sponsorshipType}") _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 var sponsorship = new OrganizationSponsorship
{ {
SponsoringOrganizationId = sponsoringOrg.Id, SponsoringOrganizationId = sponsoringOrg.Id,
SponsoringOrganizationUserId = sponsoringOrgUser.Id, SponsoringOrganizationUserId = sponsoringOrgUser.Id,
FriendlyName = friendlyName,
OfferedToEmail = sponsoredEmail, OfferedToEmail = sponsoredEmail,
PlanSponsorshipType = sponsorshipType,
CloudSponsor = true, CloudSponsor = true,
}; };
@ -78,6 +88,7 @@ namespace Bit.Core.Services
sponsorship = await _organizationSponsorshipRepository.CreateAsync(sponsorship); sponsorship = await _organizationSponsorshipRepository.CreateAsync(sponsorship);
// TODO: send email to sponsoredEmail w/ redemption token link // TODO: send email to sponsoredEmail w/ redemption token link
var _ = RedemptionToken(sponsorship.Id, sponsorshipType);
} }
catch catch
{ {
@ -91,14 +102,117 @@ namespace Bit.Core.Services
public async Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, Organization sponsoredOrganization) public async Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, Organization sponsoredOrganization)
{ {
// TODO: set up sponsorship, remember remove offeredToEmail from sponsorship if (sponsorship.PlanSponsorshipType == null)
throw new NotImplementedException(); {
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 var sponsoredOrganization = await _organizationRepository.GetByIdAsync(sponsoredOrganizationId);
throw new NotImplementedException(); 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, public async Task<string> UpgradeFreeOrganizationAsync(Organization org, StaticStore.Plan plan,
short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo) 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 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 stripePaymentMethod = false;
var paymentMethodType = PaymentMethodType.Credit; var paymentMethodType = PaymentMethodType.Credit;
var hasBtCustomerId = customer.Metadata.ContainsKey("btCustomerId"); var hasBtCustomerId = customer.Metadata.ContainsKey("btCustomerId");
@ -265,23 +326,7 @@ namespace Bit.Core.Services
} }
} }
} }
return (stripePaymentMethod, paymentMethodType);
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;
}
} }
public async Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, public async Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType,

View File

@ -1,6 +1,8 @@
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.StaticStore; using Bit.Core.Models.StaticStore;
using Bit.Core.Models.Table;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Bit.Core.Utilities namespace Bit.Core.Utilities
{ {
@ -475,5 +477,19 @@ namespace Bit.Core.Utilities
public static IDictionary<GlobalEquivalentDomainsType, IEnumerable<string>> GlobalDomains { get; set; } public static IDictionary<GlobalEquivalentDomainsType, IEnumerable<string>> GlobalDomains { get; set; }
public static IEnumerable<Plan> Plans { 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 WHERE
[OrganizationUserId] = @Id [OrganizationUserId] = @Id
EXEC [dbo].[OrganizationUser_DeleteById] @Id
DELETE DELETE
FROM FROM
[dbo].[OrganizationUser] [dbo].[OrganizationUser]
WHERE WHERE
[Id] = @Id [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 COMMIT TRANSACTION GoupUser_DeleteMany_GroupUsers
END END
EXEC [dbo].[OrganizationSponsorship_OrganizationUsersDeleted] @Ids
SET @BatchSize = 100; SET @BatchSize = 100;

View File

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

View File

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

View File

@ -16,6 +16,7 @@ using Bit.Core.Repositories;
using Bit.Core.Models.Api.Request; using Bit.Core.Models.Api.Request;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Utilities;
namespace Bit.Api.Test.Controllers namespace Bit.Api.Test.Controllers
{ {
@ -24,27 +25,29 @@ namespace Bit.Api.Test.Controllers
public class OrganizationSponsorshipsControllerTests public class OrganizationSponsorshipsControllerTests
{ {
public static IEnumerable<object[]> EnterprisePlanTypes => 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 => 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 => 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] [Theory]
[BitMemberAutoData(nameof(NonEnterprisePlanTypes))] [BitMemberAutoData(nameof(NonEnterprisePlanTypes))]
public async Task CreateSponsorship_BadSponsoringOrgPlan_ThrowsBadRequest(PlanType sponsoringOrgPlan, Organization org, public async Task CreateSponsorship_BadSponsoringOrgPlan_ThrowsBadRequest(PlanType sponsoringOrgPlan, Organization org,
SutProvider<OrganizationSponsorshipsController> sutProvider) OrganizationSponsorshipRequestModel model, SutProvider<OrganizationSponsorshipsController> sutProvider)
{ {
org.PlanType = sponsoringOrgPlan; org.PlanType = sponsoringOrgPlan;
model.PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => 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); Assert.Contains("Specified Organization cannot sponsor other organizations.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipService>() await sutProvider.GetDependency<IOrganizationSponsorshipService>()
.DidNotReceiveWithAnyArgs() .DidNotReceiveWithAnyArgs()
.OfferSponsorshipAsync(default, default, default, default); .OfferSponsorshipAsync(default, default, default, default, default);
} }
public static IEnumerable<object[]> NonConfirmedOrganizationUsersStatuses => 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); Assert.Contains("Only confirm users can sponsor other organizations.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipService>() await sutProvider.GetDependency<IOrganizationSponsorshipService>()
.DidNotReceiveWithAnyArgs() .DidNotReceiveWithAnyArgs()
.OfferSponsorshipAsync(default, default, default, default); .OfferSponsorshipAsync(default, default, default, default, default);
} }
[Theory] [Theory]
@ -96,7 +99,7 @@ namespace Bit.Api.Test.Controllers
Assert.Contains("Can only create organization sponsorships for yourself.", exception.Message); Assert.Contains("Can only create organization sponsorships for yourself.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipService>() await sutProvider.GetDependency<IOrganizationSponsorshipService>()
.DidNotReceiveWithAnyArgs() .DidNotReceiveWithAnyArgs()
.OfferSponsorshipAsync(default, default, default, default); .OfferSponsorshipAsync(default, default, default, default, default);
} }
[Theory] [Theory]
@ -121,7 +124,7 @@ namespace Bit.Api.Test.Controllers
Assert.Contains("Can only sponsor one organization per Organization User.", exception.Message); Assert.Contains("Can only sponsor one organization per Organization User.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipService>() await sutProvider.GetDependency<IOrganizationSponsorshipService>()
.DidNotReceiveWithAnyArgs() .DidNotReceiveWithAnyArgs()
.OfferSponsorshipAsync(default, default, default, default); .OfferSponsorshipAsync(default, default, default, default, default);
} }
[Theory] [Theory]
@ -272,7 +275,7 @@ namespace Bit.Api.Test.Controllers
Assert.Contains("Can only revoke a sponsorship you granted.", exception.Message); Assert.Contains("Can only revoke a sponsorship you granted.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipService>() await sutProvider.GetDependency<IOrganizationSponsorshipService>()
.DidNotReceiveWithAnyArgs() .DidNotReceiveWithAnyArgs()
.RemoveSponsorshipAsync(default); .RemoveSponsorshipAsync(default, default);
} }
[Theory] [Theory]
@ -293,10 +296,58 @@ namespace Bit.Api.Test.Controllers
var exception = await Assert.ThrowsAsync<BadRequestException>(() => var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RevokeSponsorship(sponsoringOrgUser.Id.ToString())); 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>() await sutProvider.GetDependency<IOrganizationSponsorshipService>()
.DidNotReceiveWithAnyArgs() .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] [Theory]
@ -312,7 +363,7 @@ namespace Bit.Api.Test.Controllers
Assert.Contains("Only the owner of an organization can remove sponsorship.", exception.Message); Assert.Contains("Only the owner of an organization can remove sponsorship.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipService>() await sutProvider.GetDependency<IOrganizationSponsorshipService>()
.DidNotReceiveWithAnyArgs() .DidNotReceiveWithAnyArgs()
.RemoveSponsorshipAsync(default); .RemoveSponsorshipAsync(default, default);
} }
[Theory] [Theory]
@ -334,7 +385,26 @@ namespace Bit.Api.Test.Controllers
Assert.Contains("The requested organization is not currently being sponsored.", exception.Message); Assert.Contains("The requested organization is not currently being sponsored.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipService>() await sutProvider.GetDependency<IOrganizationSponsorshipService>()
.DidNotReceiveWithAnyArgs() .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] [Theory]
[BitAutoData] [BitAutoData]
public async Task OfferSponsorship_CreatesSponsorship(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, 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, await sutProvider.Sut.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail); PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName);
var expectedSponsorship = new OrganizationSponsorship var expectedSponsorship = new OrganizationSponsorship
{ {
SponsoringOrganizationId = sponsoringOrg.Id, SponsoringOrganizationId = sponsoringOrg.Id,
SponsoringOrganizationUserId = sponsoringOrgUser.Id, SponsoringOrganizationUserId = sponsoringOrgUser.Id,
FriendlyName = friendlyName,
OfferedToEmail = sponsoredEmail, OfferedToEmail = sponsoredEmail,
CloudSponsor = true, CloudSponsor = true,
}; };
@ -55,7 +56,7 @@ namespace Bit.Core.Test.Services
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task OfferSponsorship_CreateSponsorshipThrows_RevertsDatabase(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, 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(); var expectedException = new Exception();
OrganizationSponsorship createdSponsorship = null; OrganizationSponsorship createdSponsorship = null;
@ -68,7 +69,7 @@ namespace Bit.Core.Test.Services
var actualException = await Assert.ThrowsAsync<Exception>(() => var actualException = await Assert.ThrowsAsync<Exception>(() =>
sutProvider.Sut.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, sutProvider.Sut.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail)); PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName));
Assert.Same(expectedException, actualException); Assert.Same(expectedException, actualException);
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1) await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)

View File

@ -4,9 +4,10 @@ BEGIN
CREATE TABLE [dbo].[OrganizationSponsorship] ( CREATE TABLE [dbo].[OrganizationSponsorship] (
[Id] UNIQUEIDENTIFIER NOT NULL, [Id] UNIQUEIDENTIFIER NOT NULL,
[InstallationId] UNIQUEIDENTIFIER NULL, [InstallationId] UNIQUEIDENTIFIER NULL,
[SponsoringOrganizationId] UNIQUEIDENTIFIER NOT NULL, [SponsoringOrganizationId] UNIQUEIDENTIFIER NULL,
[SponsoringOrganizationUserID] UNIQUEIDENTIFIER NOT NULL, [SponsoringOrganizationUserID] UNIQUEIDENTIFIER NULL,
[SponsoredOrganizationId] UNIQUEIDENTIFIER NULL, [SponsoredOrganizationId] UNIQUEIDENTIFIER NULL,
[FriendlyName] NVARCHAR(256) NULL,
[OfferedToEmail] NVARCHAR (256) NULL, [OfferedToEmail] NVARCHAR (256) NULL,
[PlanSponsorshipType] TINYINT NULL, [PlanSponsorshipType] TINYINT NULL,
[CloudSponsor] BIT NULL, [CloudSponsor] BIT NULL,
@ -35,6 +36,7 @@ IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationSponsors
BEGIN BEGIN
CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationId] CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationId]
ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationId] ASC) ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationId] ASC)
WHERE [SponsoringOrganizationId] IS NOT NULL;
END END
GO GO
@ -42,6 +44,7 @@ IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationSponsors
BEGIN BEGIN
CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationUserId] CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationUserId]
ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationUserID] ASC) ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationUserID] ASC)
WHERE [SponsoringOrganizationUserID] IS NOT NULL;
END END
GO GO
@ -114,6 +117,7 @@ CREATE PROCEDURE [dbo].[OrganizationSponsorship_Create]
@SponsoringOrganizationId UNIQUEIDENTIFIER, @SponsoringOrganizationId UNIQUEIDENTIFIER,
@SponsoringOrganizationUserID UNIQUEIDENTIFIER, @SponsoringOrganizationUserID UNIQUEIDENTIFIER,
@SponsoredOrganizationId UNIQUEIDENTIFIER, @SponsoredOrganizationId UNIQUEIDENTIFIER,
@FriendlyName NVARCHAR(256),
@OfferedToEmail NVARCHAR(256), @OfferedToEmail NVARCHAR(256),
@PlanSponsorshipType TINYINT, @PlanSponsorshipType TINYINT,
@CloudSponsor BIT, @CloudSponsor BIT,
@ -131,6 +135,7 @@ BEGIN
[SponsoringOrganizationId], [SponsoringOrganizationId],
[SponsoringOrganizationUserID], [SponsoringOrganizationUserID],
[SponsoredOrganizationId], [SponsoredOrganizationId],
[FriendlyName],
[OfferedToEmail], [OfferedToEmail],
[PlanSponsorshipType], [PlanSponsorshipType],
[CloudSponsor], [CloudSponsor],
@ -145,6 +150,7 @@ BEGIN
@SponsoringOrganizationId, @SponsoringOrganizationId,
@SponsoringOrganizationUserID, @SponsoringOrganizationUserID,
@SponsoredOrganizationId, @SponsoredOrganizationId,
@FriendlyName,
@OfferedToEmail, @OfferedToEmail,
@PlanSponsorshipType, @PlanSponsorshipType,
@CloudSponsor, @CloudSponsor,
@ -168,6 +174,7 @@ CREATE PROCEDURE [dbo].[OrganizationSponsorship_Update]
@SponsoringOrganizationId UNIQUEIDENTIFIER, @SponsoringOrganizationId UNIQUEIDENTIFIER,
@SponsoringOrganizationUserID UNIQUEIDENTIFIER, @SponsoringOrganizationUserID UNIQUEIDENTIFIER,
@SponsoredOrganizationId UNIQUEIDENTIFIER, @SponsoredOrganizationId UNIQUEIDENTIFIER,
@FriendlyName NVARCHAR(256),
@OfferedToEmail NVARCHAR(256), @OfferedToEmail NVARCHAR(256),
@PlanSponsorshipType TINYINT, @PlanSponsorshipType TINYINT,
@CloudSponsor BIT, @CloudSponsor BIT,
@ -185,6 +192,7 @@ BEGIN
[SponsoringOrganizationId] = @SponsoringOrganizationId, [SponsoringOrganizationId] = @SponsoringOrganizationId,
[SponsoringOrganizationUserID] = @SponsoringOrganizationUserID, [SponsoringOrganizationUserID] = @SponsoringOrganizationUserID,
[SponsoredOrganizationId] = @SponsoredOrganizationId, [SponsoredOrganizationId] = @SponsoredOrganizationId,
[FriendlyName] = @FriendlyName,
[OfferedToEmail] = @OfferedToEmail, [OfferedToEmail] = @OfferedToEmail,
[PlanSponsorshipType] = @PlanSponsorshipType, [PlanSponsorshipType] = @PlanSponsorshipType,
[CloudSponsor] = @CloudSponsor, [CloudSponsor] = @CloudSponsor,
@ -290,3 +298,365 @@ BEGIN
[OfferedToEmail] = @OfferedToEmail [OfferedToEmail] = @OfferedToEmail
END END
GO 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 namespace Bit.MySqlMigrations.Migrations
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(DatabaseContext))]
[Migration("20211104164838_OrganizationSponsorship")] [Migration("20211108225243_OrganizationSponsorship")]
partial class OrganizationSponsorship partial class OrganizationSponsorship
{ {
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -595,6 +595,10 @@ namespace Bit.MySqlMigrations.Migrations
b.Property<bool>("CloudSponsor") b.Property<bool>("CloudSponsor")
.HasColumnType("tinyint(1)"); .HasColumnType("tinyint(1)");
b.Property<string>("FriendlyName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<Guid?>("InstallationId") b.Property<Guid?>("InstallationId")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
@ -611,10 +615,10 @@ namespace Bit.MySqlMigrations.Migrations
b.Property<Guid?>("SponsoredOrganizationId") b.Property<Guid?>("SponsoredOrganizationId")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<Guid>("SponsoringOrganizationId") b.Property<Guid?>("SponsoringOrganizationId")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<Guid>("SponsoringOrganizationUserId") b.Property<Guid?>("SponsoringOrganizationUserId")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<DateTime?>("SponsorshipLapsedDate") b.Property<DateTime?>("SponsorshipLapsedDate")
@ -1356,9 +1360,7 @@ namespace Bit.MySqlMigrations.Migrations
b.HasOne("Bit.Core.Models.EntityFramework.Organization", "SponsoringOrganization") b.HasOne("Bit.Core.Models.EntityFramework.Organization", "SponsoringOrganization")
.WithMany() .WithMany()
.HasForeignKey("SponsoringOrganizationId") .HasForeignKey("SponsoringOrganizationId");
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Installation"); 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"), 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"), 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"), SponsoringOrganizationId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
SponsoringOrganizationUserId = table.Column<Guid>(type: "char(36)", nullable: false, 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"), 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) OfferedToEmail = table.Column<string>(type: "varchar(256)", maxLength: 256, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"), .Annotation("MySql:CharSet", "utf8mb4"),
PlanSponsorshipType = table.Column<byte>(type: "tinyint unsigned", nullable: true), PlanSponsorshipType = table.Column<byte>(type: "tinyint unsigned", nullable: true),
@ -51,7 +53,7 @@ namespace Bit.MySqlMigrations.Migrations
column: x => x.SponsoringOrganizationId, column: x => x.SponsoringOrganizationId,
principalTable: "Organization", principalTable: "Organization",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Restrict);
}) })
.Annotation("MySql:CharSet", "utf8mb4"); .Annotation("MySql:CharSet", "utf8mb4");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,9 +5,10 @@ ALTER TABLE "User" ADD "UsesCryptoAgent" boolean NOT NULL DEFAULT FALSE;
CREATE TABLE "OrganizationSponsorship" ( CREATE TABLE "OrganizationSponsorship" (
"Id" uuid NOT NULL, "Id" uuid NOT NULL,
"InstallationId" uuid NULL, "InstallationId" uuid NULL,
"SponsoringOrganizationId" uuid NOT NULL, "SponsoringOrganizationId" uuid NULL,
"SponsoringOrganizationUserId" uuid NOT NULL, "SponsoringOrganizationUserId" uuid NULL,
"SponsoredOrganizationId" uuid NULL, "SponsoredOrganizationId" uuid NULL,
"FriendlyName" character varying(256) NULL,
"OfferedToEmail" character varying(256) NULL, "OfferedToEmail" character varying(256) NULL,
"PlanSponsorshipType" smallint NULL, "PlanSponsorshipType" smallint NULL,
"CloudSponsor" boolean NOT NULL, "CloudSponsor" boolean NOT NULL,
@ -17,7 +18,7 @@ CREATE TABLE "OrganizationSponsorship" (
CONSTRAINT "PK_OrganizationSponsorship" PRIMARY KEY ("Id"), CONSTRAINT "PK_OrganizationSponsorship" PRIMARY KEY ("Id"),
CONSTRAINT "FK_OrganizationSponsorship_Installation_InstallationId" FOREIGN KEY ("InstallationId") REFERENCES "Installation" ("Id") ON DELETE RESTRICT, 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_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"); 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"); CREATE INDEX "IX_OrganizationSponsorship_SponsoringOrganizationId" ON "OrganizationSponsorship" ("SponsoringOrganizationId");
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20211104164532_OrganizationSponsorship', '5.0.9'); VALUES ('20211108225011_OrganizationSponsorship', '5.0.9');
COMMIT; COMMIT;