Skip to content

Commit 73b7954

Browse files
Merge branch 'main' into billing/PM-19566/invoice-approved-provider
2 parents f0ba6e7 + e943a2f commit 73b7954

File tree

50 files changed

+10529
-80
lines changed

Some content is hidden

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

50 files changed

+10529
-80
lines changed

bitwarden_license/src/Commercial.Core/SecretsManager/Queries/Projects/MaxProjectsQuery.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
using Bit.Core.Billing.Enums;
2+
using Bit.Core.Billing.Pricing;
23
using Bit.Core.Exceptions;
34
using Bit.Core.Repositories;
45
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
56
using Bit.Core.SecretsManager.Repositories;
6-
using Bit.Core.Utilities;
77

88
namespace Bit.Commercial.Core.SecretsManager.Queries.Projects;
99

1010
public class MaxProjectsQuery : IMaxProjectsQuery
1111
{
1212
private readonly IOrganizationRepository _organizationRepository;
1313
private readonly IProjectRepository _projectRepository;
14+
private readonly IPricingClient _pricingClient;
1415

1516
public MaxProjectsQuery(
1617
IOrganizationRepository organizationRepository,
17-
IProjectRepository projectRepository)
18+
IProjectRepository projectRepository,
19+
IPricingClient pricingClient)
1820
{
1921
_organizationRepository = organizationRepository;
2022
_projectRepository = projectRepository;
23+
_pricingClient = pricingClient;
2124
}
2225

2326
public async Task<(short? max, bool? overMax)> GetByOrgIdAsync(Guid organizationId, int projectsToAdd)
@@ -28,8 +31,7 @@ public MaxProjectsQuery(
2831
throw new NotFoundException();
2932
}
3033

31-
// TODO: PRICING -> https://bitwarden.atlassian.net/browse/PM-17122
32-
var plan = StaticStore.GetPlan(org.PlanType);
34+
var plan = await _pricingClient.GetPlan(org.PlanType);
3335
if (plan?.SecretsManager == null)
3436
{
3537
throw new BadRequestException("Existing plan not found.");

bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
22
using Bit.Core.AdminConsole.Entities;
33
using Bit.Core.Billing.Enums;
4+
using Bit.Core.Billing.Pricing;
45
using Bit.Core.Exceptions;
56
using Bit.Core.Repositories;
67
using Bit.Core.SecretsManager.Repositories;
8+
using Bit.Core.Utilities;
79
using Bit.Test.Common.AutoFixture;
810
using Bit.Test.Common.AutoFixture.Attributes;
911
using NSubstitute;
@@ -66,6 +68,9 @@ public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType,
6668
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
6769
{
6870
organization.PlanType = planType;
71+
72+
sutProvider.GetDependency<IPricingClient>().GetPlan(planType).Returns(StaticStore.GetPlan(planType));
73+
6974
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
7075

7176
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1);
@@ -106,6 +111,9 @@ public async Task GetByOrgIdAsync_SmFreePlan__Success(PlanType planType, int pro
106111
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
107112
{
108113
organization.PlanType = planType;
114+
115+
sutProvider.GetDependency<IPricingClient>().GetPlan(planType).Returns(StaticStore.GetPlan(planType));
116+
109117
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
110118
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
111119
.Returns(projects);

src/Admin/AdminConsole/Models/OrganizationEditModel.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public OrganizationEditModel(
8686
UseApi = org.UseApi;
8787
UseSecretsManager = org.UseSecretsManager;
8888
UseRiskInsights = org.UseRiskInsights;
89+
UseAdminSponsoredFamilies = org.UseAdminSponsoredFamilies;
8990
UseResetPassword = org.UseResetPassword;
9091
SelfHost = org.SelfHost;
9192
UsersGetPremium = org.UsersGetPremium;
@@ -154,6 +155,8 @@ public OrganizationEditModel(
154155
public new bool UseSecretsManager { get; set; }
155156
[Display(Name = "Risk Insights")]
156157
public new bool UseRiskInsights { get; set; }
158+
[Display(Name = "Admin Sponsored Families")]
159+
public bool UseAdminSponsoredFamilies { get; set; }
157160
[Display(Name = "Self Host")]
158161
public bool SelfHost { get; set; }
159162
[Display(Name = "Users Get Premium")]
@@ -295,6 +298,7 @@ public Organization ToOrganization(Organization existingOrganization)
295298
existingOrganization.UseApi = UseApi;
296299
existingOrganization.UseSecretsManager = UseSecretsManager;
297300
existingOrganization.UseRiskInsights = UseRiskInsights;
301+
existingOrganization.UseAdminSponsoredFamilies = UseAdminSponsoredFamilies;
298302
existingOrganization.UseResetPassword = UseResetPassword;
299303
existingOrganization.SelfHost = SelfHost;
300304
existingOrganization.UsersGetPremium = UsersGetPremium;

src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public OrganizationResponseModel(
6464
LimitItemDeletion = organization.LimitItemDeletion;
6565
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
6666
UseRiskInsights = organization.UseRiskInsights;
67+
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
6768
}
6869

6970
public Guid Id { get; set; }
@@ -110,6 +111,7 @@ public OrganizationResponseModel(
110111
public bool LimitItemDeletion { get; set; }
111112
public bool AllowAdminAccessToAllCollectionItems { get; set; }
112113
public bool UseRiskInsights { get; set; }
114+
public bool UseAdminSponsoredFamilies { get; set; }
113115
}
114116

115117
public class OrganizationSubscriptionResponseModel : OrganizationResponseModel

src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public ProfileOrganizationResponseModel(
7272
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
7373
UserIsClaimedByOrganization = organizationIdsClaimingUser.Contains(organization.OrganizationId);
7474
UseRiskInsights = organization.UseRiskInsights;
75+
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
7576

7677
if (organization.SsoConfig != null)
7778
{
@@ -155,4 +156,5 @@ public bool UserIsManagedByOrganization
155156
/// </returns>
156157
public bool UserIsClaimedByOrganization { get; set; }
157158
public bool UseRiskInsights { get; set; }
159+
public bool UseAdminSponsoredFamilies { get; set; }
158160
}

src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,6 @@ public ProfileProviderOrganizationResponseModel(ProviderUserOrganizationDetails
5050
LimitItemDeletion = organization.LimitItemDeletion;
5151
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
5252
UseRiskInsights = organization.UseRiskInsights;
53+
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
5354
}
5455
}

src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,27 @@ public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] Organizatio
8484
throw new BadRequestException("Free Bitwarden Families sponsorship has been disabled by your organization administrator.");
8585
}
8686

87+
if (!_featureService.IsEnabled(Bit.Core.FeatureFlagKeys.PM17772_AdminInitiatedSponsorships))
88+
{
89+
if (model.SponsoringUserId.HasValue)
90+
{
91+
throw new NotFoundException();
92+
}
93+
94+
if (!string.IsNullOrWhiteSpace(model.Notes))
95+
{
96+
model.Notes = null;
97+
}
98+
}
99+
100+
var targetUser = model.SponsoringUserId ?? _currentContext.UserId!.Value;
87101
var sponsorship = await _createSponsorshipCommand.CreateSponsorshipAsync(
88102
sponsoringOrg,
89-
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default),
90-
model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName);
103+
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, targetUser),
104+
model.PlanSponsorshipType,
105+
model.SponsoredEmail,
106+
model.FriendlyName,
107+
model.Notes);
91108
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name);
92109
}
93110

src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Bit.Core.Exceptions;
44
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
55
using Bit.Core.Repositories;
6+
using Bit.Core.Services;
67
using Bit.Core.Utilities;
78
using Microsoft.AspNetCore.Authorization;
89
using Microsoft.AspNetCore.Mvc;
@@ -20,14 +21,16 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
2021
private readonly ICreateSponsorshipCommand _offerSponsorshipCommand;
2122
private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand;
2223
private readonly ICurrentContext _currentContext;
24+
private readonly IFeatureService _featureService;
2325

2426
public SelfHostedOrganizationSponsorshipsController(
2527
ICreateSponsorshipCommand offerSponsorshipCommand,
2628
IRevokeSponsorshipCommand revokeSponsorshipCommand,
2729
IOrganizationRepository organizationRepository,
2830
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
2931
IOrganizationUserRepository organizationUserRepository,
30-
ICurrentContext currentContext
32+
ICurrentContext currentContext,
33+
IFeatureService featureService
3134
)
3235
{
3336
_offerSponsorshipCommand = offerSponsorshipCommand;
@@ -36,15 +39,29 @@ ICurrentContext currentContext
3639
_organizationSponsorshipRepository = organizationSponsorshipRepository;
3740
_organizationUserRepository = organizationUserRepository;
3841
_currentContext = currentContext;
42+
_featureService = featureService;
3943
}
4044

4145
[HttpPost("{sponsoringOrgId}/families-for-enterprise")]
4246
public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipCreateRequestModel model)
4347
{
48+
if (!_featureService.IsEnabled(Bit.Core.FeatureFlagKeys.PM17772_AdminInitiatedSponsorships))
49+
{
50+
if (model.SponsoringUserId.HasValue)
51+
{
52+
throw new NotFoundException();
53+
}
54+
55+
if (!string.IsNullOrWhiteSpace(model.Notes))
56+
{
57+
model.Notes = null;
58+
}
59+
}
60+
4461
await _offerSponsorshipCommand.CreateSponsorshipAsync(
4562
await _organizationRepository.GetByIdAsync(sponsoringOrgId),
46-
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default),
47-
model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName);
63+
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, model.SponsoringUserId ?? _currentContext.UserId ?? default),
64+
model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName, model.Notes);
4865
}
4966

5067
[HttpDelete("{sponsoringOrgId}")]

src/Api/Models/Request/Organizations/OrganizationSponsorshipCreateRequestModel.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,14 @@ public class OrganizationSponsorshipCreateRequestModel
1616

1717
[StringLength(256)]
1818
public string FriendlyName { get; set; }
19+
20+
/// <summary>
21+
/// (optional) The user to target for the sponsorship.
22+
/// </summary>
23+
/// <remarks>Left empty when creating a sponsorship for the authenticated user.</remarks>
24+
public Guid? SponsoringUserId { get; set; }
25+
26+
[EncryptedString]
27+
[EncryptedStringLength(512)]
28+
public string Notes { get; set; }
1929
}

src/Core/AdminConsole/Entities/Organization.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
114114
/// </summary>
115115
public bool UseRiskInsights { get; set; }
116116

117+
/// <summary>
118+
/// If set to true, admins can initiate organization-issued sponsorships.
119+
/// </summary>
120+
public bool UseAdminSponsoredFamilies { get; set; }
121+
117122
public void SetNewId()
118123
{
119124
if (Id == default(Guid))

src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public OrganizationAbility(Organization organization)
2626
LimitItemDeletion = organization.LimitItemDeletion;
2727
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
2828
UseRiskInsights = organization.UseRiskInsights;
29+
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
2930
}
3031

3132
public Guid Id { get; set; }
@@ -45,4 +46,5 @@ public OrganizationAbility(Organization organization)
4546
public bool LimitItemDeletion { get; set; }
4647
public bool AllowAdminAccessToAllCollectionItems { get; set; }
4748
public bool UseRiskInsights { get; set; }
49+
public bool UseAdminSponsoredFamilies { get; set; }
4850
}

src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,5 @@ public class OrganizationUserOrganizationDetails
5959
public bool LimitItemDeletion { get; set; }
6060
public bool AllowAdminAccessToAllCollectionItems { get; set; }
6161
public bool UseRiskInsights { get; set; }
62+
public bool UseAdminSponsoredFamilies { get; set; }
6263
}

src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,6 @@ public class ProviderUserOrganizationDetails
4545
public bool LimitItemDeletion { get; set; }
4646
public bool AllowAdminAccessToAllCollectionItems { get; set; }
4747
public bool UseRiskInsights { get; set; }
48+
public bool UseAdminSponsoredFamilies { get; set; }
4849
public ProviderType ProviderType { get; set; }
4950
}

src/Core/Billing/Services/Implementations/OrganizationBillingService.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,15 @@ public async Task Finalize(OrganizationSale sale)
9191

9292
var subscription = await subscriberService.GetSubscription(organization);
9393

94+
if (customer == null || subscription == null)
95+
{
96+
return OrganizationMetadata.Default with
97+
{
98+
IsEligibleForSelfHost = isEligibleForSelfHost,
99+
IsManaged = isManaged
100+
};
101+
}
102+
94103
var isOnSecretsManagerStandalone = await IsOnSecretsManagerStandalone(organization, customer, subscription);
95104

96105
var invoice = !string.IsNullOrEmpty(subscription.LatestInvoiceId)

src/Core/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ public static class FeatureFlagKeys
141141
/* Billing Team */
142142
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
143143
public const string TrialPayment = "PM-8163-trial-payment";
144+
public const string PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships";
144145
public const string UsePricingService = "use-pricing-service";
145146
public const string P15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal";
146147
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features";

src/Core/Entities/OrganizationSponsorship.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public class OrganizationSponsorship : ITableObject<Guid>
2020
public DateTime? LastSyncDate { get; set; }
2121
public DateTime? ValidUntil { get; set; }
2222
public bool ToDelete { get; set; }
23+
public bool IsAdminInitiated { get; set; }
24+
public string? Notes { get; set; }
2325

2426
public void SetNewId()
2527
{

src/Core/Models/Data/Organizations/OrganizationSponsorships/OrganizationSponsorshipData.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public OrganizationSponsorshipData(OrganizationSponsorship sponsorship)
1616
LastSyncDate = sponsorship.LastSyncDate;
1717
ValidUntil = sponsorship.ValidUntil;
1818
ToDelete = sponsorship.ToDelete;
19+
IsAdminInitiated = sponsorship.IsAdminInitiated;
20+
Notes = sponsorship.Notes;
1921
}
2022
public Guid SponsoringOrganizationUserId { get; set; }
2123
public Guid? SponsoredOrganizationId { get; set; }
@@ -25,6 +27,8 @@ public OrganizationSponsorshipData(OrganizationSponsorship sponsorship)
2527
public DateTime? LastSyncDate { get; set; }
2628
public DateTime? ValidUntil { get; set; }
2729
public bool ToDelete { get; set; }
30+
public bool IsAdminInitiated { get; set; }
31+
public string Notes { get; set; }
2832

2933
public bool CloudSponsorshipRemoved { get; set; }
3034
}

src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@ public async Task<bool> ValidateSponsorshipAsync(Guid sponsoredOrganizationId)
112112
return false;
113113
}
114114

115+
if (existingSponsorship.IsAdminInitiated && !sponsoringOrganization.UseAdminSponsoredFamilies)
116+
{
117+
_logger.LogWarning("Admin initiated sponsorship for sponsored Organization {SponsoredOrganizationId} is not allowed because sponsoring organization does not have UseAdminSponsoredFamilies enabled", sponsoredOrganizationId);
118+
await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship);
119+
return false;
120+
}
121+
115122
var sponsoringOrgProductTier = sponsoringOrganization.PlanType.GetProductTier();
116123

117124
if (sponsoredPlan.SponsoringProductTierType != sponsoringOrgProductTier)

0 commit comments

Comments
 (0)