Skip to content

Commit 8d2502d

Browse files
Merge branch 'main' into brant/PM-17562-feature-flag-for-event-integrations
2 parents 02d7692 + 9a7fddd commit 8d2502d

File tree

14 files changed

+174
-79
lines changed

14 files changed

+174
-79
lines changed

bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,14 @@ private async Task ResetOrganizationBillingAsync(
110110
IEnumerable<string> organizationOwnerEmails)
111111
{
112112
if (provider.IsBillable() &&
113-
organization.IsValidClient() &&
114-
!string.IsNullOrEmpty(organization.GatewayCustomerId))
113+
organization.IsValidClient())
115114
{
115+
// An organization converted to a business unit will not have a Customer since it was given to the business unit.
116+
if (string.IsNullOrEmpty(organization.GatewayCustomerId))
117+
{
118+
await _providerBillingService.CreateCustomerForClientOrganization(provider, organization);
119+
}
120+
116121
var customer = await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
117122
{
118123
Description = string.Empty,

src/Api/AdminConsole/Public/Controllers/MembersController.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,7 @@ public async Task<IActionResult> PutGroupIds(Guid id, [FromBody] UpdateGroupIdsR
221221
/// Remove a member.
222222
/// </summary>
223223
/// <remarks>
224-
/// Permanently removes a member from the organization. This cannot be undone.
225-
/// The user account will still remain. The user is only removed from the organization.
224+
/// Removes a member from the organization. This cannot be undone. The user account will still remain.
226225
/// </remarks>
227226
/// <param name="id">The identifier of the member to be removed.</param>
228227
[HttpDelete("{id}")]

src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,9 @@ public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] Organizatio
8686

8787
if (!_featureService.IsEnabled(Bit.Core.FeatureFlagKeys.PM17772_AdminInitiatedSponsorships))
8888
{
89-
if (model.SponsoringUserId.HasValue)
89+
if (model.IsAdminInitiated.GetValueOrDefault())
9090
{
91-
throw new NotFoundException();
91+
throw new BadRequestException();
9292
}
9393

9494
if (!string.IsNullOrWhiteSpace(model.Notes))
@@ -97,13 +97,13 @@ public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] Organizatio
9797
}
9898
}
9999

100-
var targetUser = model.SponsoringUserId ?? _currentContext.UserId!.Value;
101100
var sponsorship = await _createSponsorshipCommand.CreateSponsorshipAsync(
102101
sponsoringOrg,
103-
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, targetUser),
102+
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default),
104103
model.PlanSponsorshipType,
105104
model.SponsoredEmail,
106105
model.FriendlyName,
106+
model.IsAdminInitiated.GetValueOrDefault(),
107107
model.Notes);
108108
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name);
109109
}

src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] Organizatio
4747
{
4848
if (!_featureService.IsEnabled(Bit.Core.FeatureFlagKeys.PM17772_AdminInitiatedSponsorships))
4949
{
50-
if (model.SponsoringUserId.HasValue)
50+
if (model.IsAdminInitiated.GetValueOrDefault())
5151
{
52-
throw new NotFoundException();
52+
throw new BadRequestException();
5353
}
5454

5555
if (!string.IsNullOrWhiteSpace(model.Notes))
@@ -60,8 +60,12 @@ public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] Organizatio
6060

6161
await _offerSponsorshipCommand.CreateSponsorshipAsync(
6262
await _organizationRepository.GetByIdAsync(sponsoringOrgId),
63-
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, model.SponsoringUserId ?? _currentContext.UserId ?? default),
64-
model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName, model.Notes);
63+
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default),
64+
model.PlanSponsorshipType,
65+
model.SponsoredEmail,
66+
model.FriendlyName,
67+
model.IsAdminInitiated.GetValueOrDefault(),
68+
model.Notes);
6569
}
6670

6771
[HttpDelete("{sponsoringOrgId}")]

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,7 @@ public class OrganizationSponsorshipCreateRequestModel
1717
[StringLength(256)]
1818
public string FriendlyName { get; set; }
1919

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; }
20+
public bool? IsAdminInitiated { get; set; }
2521

2622
[EncryptedString]
2723
[EncryptedStringLength(512)]

src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
1818
Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId);
1919
Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type);
2020
Task<int> GetCountByOrganizationAsync(Guid organizationId, string email, bool onlyRegisteredUsers);
21+
22+
/// <summary>
23+
/// Returns the number of occupied seats for an organization.
24+
/// Occupied seats are OrganizationUsers that have at least been invited.
25+
/// As of https://bitwarden.atlassian.net/browse/PM-17772, a seat is also occupied by a Families for Enterprise sponsorship sent by an
26+
/// organization admin, even if the user sent the invitation doesn't have a corresponding OrganizationUser in the Enterprise organization.
27+
/// </summary>
28+
/// <param name="organizationId">The ID of the organization to get the occupied seat count for.</param>
29+
/// <returns>The number of occupied seats for the organization.</returns>
2130
Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId);
2231
Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers);
2332
Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId);

src/Core/Constants.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,6 @@ public static class FeatureFlagKeys
196196
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
197197
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
198198
public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss";
199-
public const string VaultBulkManagementAction = "vault-bulk-management-action";
200199
public const string RestrictProviderAccess = "restrict-provider-access";
201200
public const string SecurityTasks = "security-tasks";
202201
public const string CipherKeyEncryption = "cipher-key-encryption";

src/Core/Core.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323

2424
<ItemGroup>
2525
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
26-
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.402.61" />
27-
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.118" />
26+
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.402.79" />
27+
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.136" />
2828
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
2929
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
3030
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />

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

Lines changed: 55 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,17 @@ namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnte
1414
public class CreateSponsorshipCommand(
1515
ICurrentContext currentContext,
1616
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
17-
IUserService userService) : ICreateSponsorshipCommand
17+
IUserService userService,
18+
IOrganizationService organizationService) : ICreateSponsorshipCommand
1819
{
19-
public async Task<OrganizationSponsorship> CreateSponsorshipAsync(Organization sponsoringOrganization,
20-
OrganizationUser sponsoringMember, PlanSponsorshipType sponsorshipType, string sponsoredEmail,
21-
string friendlyName, string notes)
20+
public async Task<OrganizationSponsorship> CreateSponsorshipAsync(
21+
Organization sponsoringOrganization,
22+
OrganizationUser sponsoringMember,
23+
PlanSponsorshipType sponsorshipType,
24+
string sponsoredEmail,
25+
string friendlyName,
26+
bool isAdminInitiated,
27+
string notes)
2228
{
2329
var sponsoringUser = await userService.GetUserByIdAsync(sponsoringMember.UserId!.Value);
2430

@@ -48,48 +54,44 @@ public async Task<OrganizationSponsorship> CreateSponsorshipAsync(Organization s
4854
throw new BadRequestException("Can only sponsor one organization per Organization User.");
4955
}
5056

51-
var sponsorship = new OrganizationSponsorship();
52-
sponsorship.SponsoringOrganizationId = sponsoringOrganization.Id;
53-
sponsorship.SponsoringOrganizationUserId = sponsoringMember.Id;
54-
sponsorship.FriendlyName = friendlyName;
55-
sponsorship.OfferedToEmail = sponsoredEmail;
56-
sponsorship.PlanSponsorshipType = sponsorshipType;
57+
if (isAdminInitiated)
58+
{
59+
ValidateAdminInitiatedSponsorship(sponsoringOrganization);
60+
}
61+
62+
var sponsorship = new OrganizationSponsorship
63+
{
64+
SponsoringOrganizationId = sponsoringOrganization.Id,
65+
SponsoringOrganizationUserId = sponsoringMember.Id,
66+
FriendlyName = friendlyName,
67+
OfferedToEmail = sponsoredEmail,
68+
PlanSponsorshipType = sponsorshipType,
69+
IsAdminInitiated = isAdminInitiated,
70+
Notes = notes
71+
};
5772

5873
if (existingOrgSponsorship != null)
5974
{
6075
// Replace existing invalid offer with our new sponsorship offer
6176
sponsorship.Id = existingOrgSponsorship.Id;
6277
}
6378

64-
var isAdminInitiated = false;
65-
if (currentContext.UserId != sponsoringMember.UserId)
79+
if (isAdminInitiated && sponsoringOrganization.Seats.HasValue)
80+
{
81+
await organizationService.AutoAddSeatsAsync(sponsoringOrganization, 1);
82+
}
83+
84+
try
6685
{
67-
var organization = currentContext.Organizations.First(x => x.Id == sponsoringOrganization.Id);
68-
OrganizationUserType[] allowedUserTypes =
69-
[
70-
OrganizationUserType.Admin,
71-
OrganizationUserType.Owner
72-
];
73-
74-
if (!organization.Permissions.ManageUsers && allowedUserTypes.All(x => x != organization.Type))
86+
if (isAdminInitiated)
7587
{
76-
throw new UnauthorizedAccessException("You do not have permissions to send sponsorships on behalf of the organization.");
88+
await organizationSponsorshipRepository.CreateAsync(sponsorship);
7789
}
78-
79-
if (!sponsoringOrganization.UseAdminSponsoredFamilies)
90+
else
8091
{
81-
throw new BadRequestException("Sponsoring organization cannot sponsor other Family organizations.");
92+
await organizationSponsorshipRepository.UpsertAsync(sponsorship);
8293
}
8394

84-
isAdminInitiated = true;
85-
}
86-
87-
sponsorship.IsAdminInitiated = isAdminInitiated;
88-
sponsorship.Notes = notes;
89-
90-
try
91-
{
92-
await organizationSponsorshipRepository.UpsertAsync(sponsorship);
9395
return sponsorship;
9496
}
9597
catch
@@ -101,4 +103,24 @@ public async Task<OrganizationSponsorship> CreateSponsorshipAsync(Organization s
101103
throw;
102104
}
103105
}
106+
107+
private void ValidateAdminInitiatedSponsorship(Organization sponsoringOrganization)
108+
{
109+
var organization = currentContext.Organizations.First(x => x.Id == sponsoringOrganization.Id);
110+
OrganizationUserType[] allowedUserTypes =
111+
[
112+
OrganizationUserType.Admin,
113+
OrganizationUserType.Owner
114+
];
115+
116+
if (!organization.Permissions.ManageUsers && allowedUserTypes.All(x => x != organization.Type))
117+
{
118+
throw new UnauthorizedAccessException("You do not have permissions to send sponsorships on behalf of the organization");
119+
}
120+
121+
if (!sponsoringOrganization.UseAdminSponsoredFamilies)
122+
{
123+
throw new BadRequestException("Sponsoring organization cannot send admin-initiated sponsorship invitations");
124+
}
125+
}
104126
}

src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/ICreateSponsorshipCommand.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnte
66

77
public interface ICreateSponsorshipCommand
88
{
9-
Task<OrganizationSponsorship> CreateSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser,
10-
PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName, string notes);
9+
Task<OrganizationSponsorship> CreateSponsorshipAsync(
10+
Organization sponsoringOrg,
11+
OrganizationUser sponsoringOrgUser,
12+
PlanSponsorshipType sponsorshipType,
13+
string sponsoredEmail,
14+
string friendlyName,
15+
bool isAdminInitiated,
16+
string notes);
1117
}

src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,23 @@ public OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery(Guid organizat
1414

1515
public IQueryable<OrganizationUser> Run(DatabaseContext dbContext)
1616
{
17-
var query = from ou in dbContext.OrganizationUsers
18-
where ou.OrganizationId == _organizationId && ou.Status >= OrganizationUserStatusType.Invited
19-
select ou;
20-
return query;
17+
var orgUsersQuery = from ou in dbContext.OrganizationUsers
18+
where ou.OrganizationId == _organizationId && ou.Status >= OrganizationUserStatusType.Invited
19+
select new OrganizationUser { Id = ou.Id, OrganizationId = ou.OrganizationId, Status = ou.Status };
20+
21+
// As of https://bitwarden.atlassian.net/browse/PM-17772, a seat is also occupied by a Families for Enterprise sponsorship sent by an
22+
// organization admin, even if the user sent the invitation doesn't have a corresponding OrganizationUser in the Enterprise organization.
23+
var sponsorshipsQuery = from os in dbContext.OrganizationSponsorships
24+
where os.SponsoringOrganizationId == _organizationId &&
25+
os.IsAdminInitiated &&
26+
!os.ToDelete
27+
select new OrganizationUser
28+
{
29+
Id = os.Id,
30+
OrganizationId = _organizationId,
31+
Status = OrganizationUserStatusType.Invited
32+
};
33+
34+
return orgUsersQuery.Concat(sponsorshipsQuery);
2135
}
2236
}

src/Sql/dbo/Stored Procedures/OrganizationUser_ReadOccupiedSeatCountByOrganizationId.sql

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@ BEGIN
55
SET NOCOUNT ON
66

77
SELECT
8-
COUNT(1)
9-
FROM
10-
[dbo].[OrganizationUserView]
11-
WHERE
12-
OrganizationId = @OrganizationId
13-
AND Status >= 0 --Invited
8+
(
9+
-- Count organization users
10+
SELECT COUNT(1)
11+
FROM [dbo].[OrganizationUserView]
12+
WHERE OrganizationId = @OrganizationId
13+
AND Status >= 0 --Invited
14+
) +
15+
(
16+
-- Count admin-initiated sponsorships towards the seat count
17+
-- Introduced in https://bitwarden.atlassian.net/browse/PM-17772
18+
SELECT COUNT(1)
19+
FROM [dbo].[OrganizationSponsorship]
20+
WHERE SponsoringOrganizationId = @OrganizationId
21+
AND IsAdminInitiated = 1
22+
)
1423
END

0 commit comments

Comments
 (0)