mirror of
https://github.com/bitwarden/server.git
synced 2025-07-03 00:52:49 -05:00
[PM-17777] sponsorships consume seats (#5694)
* Admin initiated sponsorships now use seats similarly to inviting an organization user * Updated f4e endpoint to not expect a user ID, and instead just send a boolean * Fixed failing tests * Updated OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery to ensure both left and right sides are selecting the same columns
This commit is contained in:
@ -18,6 +18,15 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
||||
Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId);
|
||||
Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type);
|
||||
Task<int> GetCountByOrganizationAsync(Guid organizationId, string email, bool onlyRegisteredUsers);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of occupied seats for an organization.
|
||||
/// Occupied seats are OrganizationUsers that have at least been invited.
|
||||
/// As of https://bitwarden.atlassian.net/browse/PM-17772, a seat is also occupied by a Families for Enterprise sponsorship sent by an
|
||||
/// organization admin, even if the user sent the invitation doesn't have a corresponding OrganizationUser in the Enterprise organization.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The ID of the organization to get the occupied seat count for.</param>
|
||||
/// <returns>The number of occupied seats for the organization.</returns>
|
||||
Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId);
|
||||
Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers);
|
||||
Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId);
|
||||
|
@ -14,11 +14,17 @@ namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnte
|
||||
public class CreateSponsorshipCommand(
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
||||
IUserService userService) : ICreateSponsorshipCommand
|
||||
IUserService userService,
|
||||
IOrganizationService organizationService) : ICreateSponsorshipCommand
|
||||
{
|
||||
public async Task<OrganizationSponsorship> CreateSponsorshipAsync(Organization sponsoringOrganization,
|
||||
OrganizationUser sponsoringMember, PlanSponsorshipType sponsorshipType, string sponsoredEmail,
|
||||
string friendlyName, string notes)
|
||||
public async Task<OrganizationSponsorship> CreateSponsorshipAsync(
|
||||
Organization sponsoringOrganization,
|
||||
OrganizationUser sponsoringMember,
|
||||
PlanSponsorshipType sponsorshipType,
|
||||
string sponsoredEmail,
|
||||
string friendlyName,
|
||||
bool isAdminInitiated,
|
||||
string notes)
|
||||
{
|
||||
var sponsoringUser = await userService.GetUserByIdAsync(sponsoringMember.UserId!.Value);
|
||||
|
||||
@ -48,12 +54,21 @@ public class CreateSponsorshipCommand(
|
||||
throw new BadRequestException("Can only sponsor one organization per Organization User.");
|
||||
}
|
||||
|
||||
var sponsorship = new OrganizationSponsorship();
|
||||
sponsorship.SponsoringOrganizationId = sponsoringOrganization.Id;
|
||||
sponsorship.SponsoringOrganizationUserId = sponsoringMember.Id;
|
||||
sponsorship.FriendlyName = friendlyName;
|
||||
sponsorship.OfferedToEmail = sponsoredEmail;
|
||||
sponsorship.PlanSponsorshipType = sponsorshipType;
|
||||
if (isAdminInitiated)
|
||||
{
|
||||
ValidateAdminInitiatedSponsorship(sponsoringOrganization);
|
||||
}
|
||||
|
||||
var sponsorship = new OrganizationSponsorship
|
||||
{
|
||||
SponsoringOrganizationId = sponsoringOrganization.Id,
|
||||
SponsoringOrganizationUserId = sponsoringMember.Id,
|
||||
FriendlyName = friendlyName,
|
||||
OfferedToEmail = sponsoredEmail,
|
||||
PlanSponsorshipType = sponsorshipType,
|
||||
IsAdminInitiated = isAdminInitiated,
|
||||
Notes = notes
|
||||
};
|
||||
|
||||
if (existingOrgSponsorship != null)
|
||||
{
|
||||
@ -61,35 +76,22 @@ public class CreateSponsorshipCommand(
|
||||
sponsorship.Id = existingOrgSponsorship.Id;
|
||||
}
|
||||
|
||||
var isAdminInitiated = false;
|
||||
if (currentContext.UserId != sponsoringMember.UserId)
|
||||
if (isAdminInitiated && sponsoringOrganization.Seats.HasValue)
|
||||
{
|
||||
var organization = currentContext.Organizations.First(x => x.Id == sponsoringOrganization.Id);
|
||||
OrganizationUserType[] allowedUserTypes =
|
||||
[
|
||||
OrganizationUserType.Admin,
|
||||
OrganizationUserType.Owner
|
||||
];
|
||||
|
||||
if (!organization.Permissions.ManageUsers && allowedUserTypes.All(x => x != organization.Type))
|
||||
{
|
||||
throw new UnauthorizedAccessException("You do not have permissions to send sponsorships on behalf of the organization.");
|
||||
}
|
||||
|
||||
if (!sponsoringOrganization.UseAdminSponsoredFamilies)
|
||||
{
|
||||
throw new BadRequestException("Sponsoring organization cannot sponsor other Family organizations.");
|
||||
}
|
||||
|
||||
isAdminInitiated = true;
|
||||
await organizationService.AutoAddSeatsAsync(sponsoringOrganization, 1);
|
||||
}
|
||||
|
||||
sponsorship.IsAdminInitiated = isAdminInitiated;
|
||||
sponsorship.Notes = notes;
|
||||
|
||||
try
|
||||
{
|
||||
await organizationSponsorshipRepository.UpsertAsync(sponsorship);
|
||||
if (isAdminInitiated)
|
||||
{
|
||||
await organizationSponsorshipRepository.CreateAsync(sponsorship);
|
||||
}
|
||||
else
|
||||
{
|
||||
await organizationSponsorshipRepository.UpsertAsync(sponsorship);
|
||||
}
|
||||
|
||||
return sponsorship;
|
||||
}
|
||||
catch
|
||||
@ -101,4 +103,24 @@ public class CreateSponsorshipCommand(
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateAdminInitiatedSponsorship(Organization sponsoringOrganization)
|
||||
{
|
||||
var organization = currentContext.Organizations.First(x => x.Id == sponsoringOrganization.Id);
|
||||
OrganizationUserType[] allowedUserTypes =
|
||||
[
|
||||
OrganizationUserType.Admin,
|
||||
OrganizationUserType.Owner
|
||||
];
|
||||
|
||||
if (!organization.Permissions.ManageUsers && allowedUserTypes.All(x => x != organization.Type))
|
||||
{
|
||||
throw new UnauthorizedAccessException("You do not have permissions to send sponsorships on behalf of the organization");
|
||||
}
|
||||
|
||||
if (!sponsoringOrganization.UseAdminSponsoredFamilies)
|
||||
{
|
||||
throw new BadRequestException("Sponsoring organization cannot send admin-initiated sponsorship invitations");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,12 @@ namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnte
|
||||
|
||||
public interface ICreateSponsorshipCommand
|
||||
{
|
||||
Task<OrganizationSponsorship> CreateSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser,
|
||||
PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName, string notes);
|
||||
Task<OrganizationSponsorship> CreateSponsorshipAsync(
|
||||
Organization sponsoringOrg,
|
||||
OrganizationUser sponsoringOrgUser,
|
||||
PlanSponsorshipType sponsorshipType,
|
||||
string sponsoredEmail,
|
||||
string friendlyName,
|
||||
bool isAdminInitiated,
|
||||
string notes);
|
||||
}
|
||||
|
Reference in New Issue
Block a user