mirror of
https://github.com/bitwarden/server.git
synced 2025-04-26 07:12:20 -05:00
Merge branch 'main' into brant/PM-17562-feature-flag-for-event-integrations
This commit is contained in:
commit
8d2502d8e6
@ -110,9 +110,14 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
IEnumerable<string> organizationOwnerEmails)
|
IEnumerable<string> organizationOwnerEmails)
|
||||||
{
|
{
|
||||||
if (provider.IsBillable() &&
|
if (provider.IsBillable() &&
|
||||||
organization.IsValidClient() &&
|
organization.IsValidClient())
|
||||||
!string.IsNullOrEmpty(organization.GatewayCustomerId))
|
|
||||||
{
|
{
|
||||||
|
// An organization converted to a business unit will not have a Customer since it was given to the business unit.
|
||||||
|
if (string.IsNullOrEmpty(organization.GatewayCustomerId))
|
||||||
|
{
|
||||||
|
await _providerBillingService.CreateCustomerForClientOrganization(provider, organization);
|
||||||
|
}
|
||||||
|
|
||||||
var customer = await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
var customer = await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
||||||
{
|
{
|
||||||
Description = string.Empty,
|
Description = string.Empty,
|
||||||
|
@ -221,8 +221,7 @@ public class MembersController : Controller
|
|||||||
/// Remove a member.
|
/// Remove a member.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Permanently removes a member from the organization. This cannot be undone.
|
/// Removes a member from the organization. This cannot be undone. The user account will still remain.
|
||||||
/// The user account will still remain. The user is only removed from the organization.
|
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="id">The identifier of the member to be removed.</param>
|
/// <param name="id">The identifier of the member to be removed.</param>
|
||||||
[HttpDelete("{id}")]
|
[HttpDelete("{id}")]
|
||||||
|
@ -86,9 +86,9 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
|
|
||||||
if (!_featureService.IsEnabled(Bit.Core.FeatureFlagKeys.PM17772_AdminInitiatedSponsorships))
|
if (!_featureService.IsEnabled(Bit.Core.FeatureFlagKeys.PM17772_AdminInitiatedSponsorships))
|
||||||
{
|
{
|
||||||
if (model.SponsoringUserId.HasValue)
|
if (model.IsAdminInitiated.GetValueOrDefault())
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new BadRequestException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(model.Notes))
|
if (!string.IsNullOrWhiteSpace(model.Notes))
|
||||||
@ -97,13 +97,13 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetUser = model.SponsoringUserId ?? _currentContext.UserId!.Value;
|
|
||||||
var sponsorship = await _createSponsorshipCommand.CreateSponsorshipAsync(
|
var sponsorship = await _createSponsorshipCommand.CreateSponsorshipAsync(
|
||||||
sponsoringOrg,
|
sponsoringOrg,
|
||||||
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, targetUser),
|
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default),
|
||||||
model.PlanSponsorshipType,
|
model.PlanSponsorshipType,
|
||||||
model.SponsoredEmail,
|
model.SponsoredEmail,
|
||||||
model.FriendlyName,
|
model.FriendlyName,
|
||||||
|
model.IsAdminInitiated.GetValueOrDefault(),
|
||||||
model.Notes);
|
model.Notes);
|
||||||
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name);
|
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name);
|
||||||
}
|
}
|
||||||
|
@ -47,9 +47,9 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
|
|||||||
{
|
{
|
||||||
if (!_featureService.IsEnabled(Bit.Core.FeatureFlagKeys.PM17772_AdminInitiatedSponsorships))
|
if (!_featureService.IsEnabled(Bit.Core.FeatureFlagKeys.PM17772_AdminInitiatedSponsorships))
|
||||||
{
|
{
|
||||||
if (model.SponsoringUserId.HasValue)
|
if (model.IsAdminInitiated.GetValueOrDefault())
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new BadRequestException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(model.Notes))
|
if (!string.IsNullOrWhiteSpace(model.Notes))
|
||||||
@ -60,8 +60,12 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
|
|||||||
|
|
||||||
await _offerSponsorshipCommand.CreateSponsorshipAsync(
|
await _offerSponsorshipCommand.CreateSponsorshipAsync(
|
||||||
await _organizationRepository.GetByIdAsync(sponsoringOrgId),
|
await _organizationRepository.GetByIdAsync(sponsoringOrgId),
|
||||||
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, model.SponsoringUserId ?? _currentContext.UserId ?? default),
|
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default),
|
||||||
model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName, model.Notes);
|
model.PlanSponsorshipType,
|
||||||
|
model.SponsoredEmail,
|
||||||
|
model.FriendlyName,
|
||||||
|
model.IsAdminInitiated.GetValueOrDefault(),
|
||||||
|
model.Notes);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{sponsoringOrgId}")]
|
[HttpDelete("{sponsoringOrgId}")]
|
||||||
|
@ -17,11 +17,7 @@ public class OrganizationSponsorshipCreateRequestModel
|
|||||||
[StringLength(256)]
|
[StringLength(256)]
|
||||||
public string FriendlyName { get; set; }
|
public string FriendlyName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
public bool? IsAdminInitiated { get; set; }
|
||||||
/// (optional) The user to target for the sponsorship.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>Left empty when creating a sponsorship for the authenticated user.</remarks>
|
|
||||||
public Guid? SponsoringUserId { get; set; }
|
|
||||||
|
|
||||||
[EncryptedString]
|
[EncryptedString]
|
||||||
[EncryptedStringLength(512)]
|
[EncryptedStringLength(512)]
|
||||||
|
@ -18,6 +18,15 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
|||||||
Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId);
|
Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId);
|
||||||
Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type);
|
Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type);
|
||||||
Task<int> GetCountByOrganizationAsync(Guid organizationId, string email, bool onlyRegisteredUsers);
|
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<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId);
|
||||||
Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers);
|
Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers);
|
||||||
Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId);
|
Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId);
|
||||||
|
@ -196,7 +196,6 @@ public static class FeatureFlagKeys
|
|||||||
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
|
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
|
||||||
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
|
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
|
||||||
public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss";
|
public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss";
|
||||||
public const string VaultBulkManagementAction = "vault-bulk-management-action";
|
|
||||||
public const string RestrictProviderAccess = "restrict-provider-access";
|
public const string RestrictProviderAccess = "restrict-provider-access";
|
||||||
public const string SecurityTasks = "security-tasks";
|
public const string SecurityTasks = "security-tasks";
|
||||||
public const string CipherKeyEncryption = "cipher-key-encryption";
|
public const string CipherKeyEncryption = "cipher-key-encryption";
|
||||||
|
@ -23,8 +23,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
||||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.402.61" />
|
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.402.79" />
|
||||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.118" />
|
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.136" />
|
||||||
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
||||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
||||||
|
@ -14,11 +14,17 @@ namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnte
|
|||||||
public class CreateSponsorshipCommand(
|
public class CreateSponsorshipCommand(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
||||||
IUserService userService) : ICreateSponsorshipCommand
|
IUserService userService,
|
||||||
|
IOrganizationService organizationService) : ICreateSponsorshipCommand
|
||||||
{
|
{
|
||||||
public async Task<OrganizationSponsorship> CreateSponsorshipAsync(Organization sponsoringOrganization,
|
public async Task<OrganizationSponsorship> CreateSponsorshipAsync(
|
||||||
OrganizationUser sponsoringMember, PlanSponsorshipType sponsorshipType, string sponsoredEmail,
|
Organization sponsoringOrganization,
|
||||||
string friendlyName, string notes)
|
OrganizationUser sponsoringMember,
|
||||||
|
PlanSponsorshipType sponsorshipType,
|
||||||
|
string sponsoredEmail,
|
||||||
|
string friendlyName,
|
||||||
|
bool isAdminInitiated,
|
||||||
|
string notes)
|
||||||
{
|
{
|
||||||
var sponsoringUser = await userService.GetUserByIdAsync(sponsoringMember.UserId!.Value);
|
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.");
|
throw new BadRequestException("Can only sponsor one organization per Organization User.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var sponsorship = new OrganizationSponsorship();
|
if (isAdminInitiated)
|
||||||
sponsorship.SponsoringOrganizationId = sponsoringOrganization.Id;
|
{
|
||||||
sponsorship.SponsoringOrganizationUserId = sponsoringMember.Id;
|
ValidateAdminInitiatedSponsorship(sponsoringOrganization);
|
||||||
sponsorship.FriendlyName = friendlyName;
|
}
|
||||||
sponsorship.OfferedToEmail = sponsoredEmail;
|
|
||||||
sponsorship.PlanSponsorshipType = sponsorshipType;
|
var sponsorship = new OrganizationSponsorship
|
||||||
|
{
|
||||||
|
SponsoringOrganizationId = sponsoringOrganization.Id,
|
||||||
|
SponsoringOrganizationUserId = sponsoringMember.Id,
|
||||||
|
FriendlyName = friendlyName,
|
||||||
|
OfferedToEmail = sponsoredEmail,
|
||||||
|
PlanSponsorshipType = sponsorshipType,
|
||||||
|
IsAdminInitiated = isAdminInitiated,
|
||||||
|
Notes = notes
|
||||||
|
};
|
||||||
|
|
||||||
if (existingOrgSponsorship != null)
|
if (existingOrgSponsorship != null)
|
||||||
{
|
{
|
||||||
@ -61,35 +76,22 @@ public class CreateSponsorshipCommand(
|
|||||||
sponsorship.Id = existingOrgSponsorship.Id;
|
sponsorship.Id = existingOrgSponsorship.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
var isAdminInitiated = false;
|
if (isAdminInitiated && sponsoringOrganization.Seats.HasValue)
|
||||||
if (currentContext.UserId != sponsoringMember.UserId)
|
|
||||||
{
|
{
|
||||||
var organization = currentContext.Organizations.First(x => x.Id == sponsoringOrganization.Id);
|
await organizationService.AutoAddSeatsAsync(sponsoringOrganization, 1);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sponsorship.IsAdminInitiated = isAdminInitiated;
|
|
||||||
sponsorship.Notes = notes;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await organizationSponsorshipRepository.UpsertAsync(sponsorship);
|
if (isAdminInitiated)
|
||||||
|
{
|
||||||
|
await organizationSponsorshipRepository.CreateAsync(sponsorship);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await organizationSponsorshipRepository.UpsertAsync(sponsorship);
|
||||||
|
}
|
||||||
|
|
||||||
return sponsorship;
|
return sponsorship;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@ -101,4 +103,24 @@ public class CreateSponsorshipCommand(
|
|||||||
throw;
|
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
|
public interface ICreateSponsorshipCommand
|
||||||
{
|
{
|
||||||
Task<OrganizationSponsorship> CreateSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser,
|
Task<OrganizationSponsorship> CreateSponsorshipAsync(
|
||||||
PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName, string notes);
|
Organization sponsoringOrg,
|
||||||
|
OrganizationUser sponsoringOrgUser,
|
||||||
|
PlanSponsorshipType sponsorshipType,
|
||||||
|
string sponsoredEmail,
|
||||||
|
string friendlyName,
|
||||||
|
bool isAdminInitiated,
|
||||||
|
string notes);
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,23 @@ public class OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery : IQuery
|
|||||||
|
|
||||||
public IQueryable<OrganizationUser> Run(DatabaseContext dbContext)
|
public IQueryable<OrganizationUser> Run(DatabaseContext dbContext)
|
||||||
{
|
{
|
||||||
var query = from ou in dbContext.OrganizationUsers
|
var orgUsersQuery = from ou in dbContext.OrganizationUsers
|
||||||
where ou.OrganizationId == _organizationId && ou.Status >= OrganizationUserStatusType.Invited
|
where ou.OrganizationId == _organizationId && ou.Status >= OrganizationUserStatusType.Invited
|
||||||
select ou;
|
select new OrganizationUser { Id = ou.Id, OrganizationId = ou.OrganizationId, Status = ou.Status };
|
||||||
return query;
|
|
||||||
|
// 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.
|
||||||
|
var sponsorshipsQuery = from os in dbContext.OrganizationSponsorships
|
||||||
|
where os.SponsoringOrganizationId == _organizationId &&
|
||||||
|
os.IsAdminInitiated &&
|
||||||
|
!os.ToDelete
|
||||||
|
select new OrganizationUser
|
||||||
|
{
|
||||||
|
Id = os.Id,
|
||||||
|
OrganizationId = _organizationId,
|
||||||
|
Status = OrganizationUserStatusType.Invited
|
||||||
|
};
|
||||||
|
|
||||||
|
return orgUsersQuery.Concat(sponsorshipsQuery);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,19 @@ BEGIN
|
|||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(1)
|
(
|
||||||
FROM
|
-- Count organization users
|
||||||
[dbo].[OrganizationUserView]
|
SELECT COUNT(1)
|
||||||
WHERE
|
FROM [dbo].[OrganizationUserView]
|
||||||
OrganizationId = @OrganizationId
|
WHERE OrganizationId = @OrganizationId
|
||||||
AND Status >= 0 --Invited
|
AND Status >= 0 --Invited
|
||||||
|
) +
|
||||||
|
(
|
||||||
|
-- Count admin-initiated sponsorships towards the seat count
|
||||||
|
-- Introduced in https://bitwarden.atlassian.net/browse/PM-17772
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM [dbo].[OrganizationSponsorship]
|
||||||
|
WHERE SponsoringOrganizationId = @OrganizationId
|
||||||
|
AND IsAdminInitiated = 1
|
||||||
|
)
|
||||||
END
|
END
|
||||||
|
@ -41,7 +41,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
|||||||
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).ReturnsNull();
|
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).ReturnsNull();
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
sutProvider.Sut.CreateSponsorshipAsync(null, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default, null));
|
sutProvider.Sut.CreateSponsorshipAsync(null, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default, false, null));
|
||||||
|
|
||||||
Assert.Contains("Cannot offer a Families Organization Sponsorship to yourself. Choose a different email.", exception.Message);
|
Assert.Contains("Cannot offer a Families Organization Sponsorship to yourself. Choose a different email.", exception.Message);
|
||||||
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
|
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
|
||||||
@ -55,7 +55,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
|||||||
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).Returns(user);
|
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).Returns(user);
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
sutProvider.Sut.CreateSponsorshipAsync(null, orgUser, PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, default, null));
|
sutProvider.Sut.CreateSponsorshipAsync(null, orgUser, PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, default, false, null));
|
||||||
|
|
||||||
Assert.Contains("Cannot offer a Families Organization Sponsorship to yourself. Choose a different email.", exception.Message);
|
Assert.Contains("Cannot offer a Families Organization Sponsorship to yourself. Choose a different email.", exception.Message);
|
||||||
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
|
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
|
||||||
@ -72,7 +72,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
|||||||
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).Returns(user);
|
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).Returns(user);
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
sutProvider.Sut.CreateSponsorshipAsync(org, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default, null));
|
sutProvider.Sut.CreateSponsorshipAsync(org, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default, false, null));
|
||||||
|
|
||||||
Assert.Contains("Specified Organization cannot sponsor other organizations.", exception.Message);
|
Assert.Contains("Specified Organization cannot sponsor other organizations.", exception.Message);
|
||||||
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
|
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
|
||||||
@ -91,7 +91,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
|||||||
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).Returns(user);
|
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).Returns(user);
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
sutProvider.Sut.CreateSponsorshipAsync(org, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default, null));
|
sutProvider.Sut.CreateSponsorshipAsync(org, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default, false, null));
|
||||||
|
|
||||||
Assert.Contains("Only confirmed users can sponsor other organizations.", exception.Message);
|
Assert.Contains("Only confirmed users can sponsor other organizations.", exception.Message);
|
||||||
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
|
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
|
||||||
@ -115,7 +115,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
|||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(orgUser.UserId.Value);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(orgUser.UserId.Value);
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
sutProvider.Sut.CreateSponsorshipAsync(org, orgUser, sponsorship.PlanSponsorshipType!.Value, null, null, null));
|
sutProvider.Sut.CreateSponsorshipAsync(org, orgUser, sponsorship.PlanSponsorshipType!.Value, null, null, false, null));
|
||||||
|
|
||||||
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<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
|
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
|
||||||
@ -147,7 +147,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
|||||||
|
|
||||||
|
|
||||||
var actual = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
var actual = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||||
await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, null));
|
await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, false, null));
|
||||||
|
|
||||||
Assert.Equal("Only confirmed users can sponsor other organizations.", actual.Message);
|
Assert.Equal("Only confirmed users can sponsor other organizations.", actual.Message);
|
||||||
}
|
}
|
||||||
@ -170,7 +170,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
|||||||
|
|
||||||
|
|
||||||
await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
|
await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
|
||||||
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, null);
|
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, false, null);
|
||||||
|
|
||||||
var expectedSponsorship = new OrganizationSponsorship
|
var expectedSponsorship = new OrganizationSponsorship
|
||||||
{
|
{
|
||||||
@ -209,7 +209,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
|||||||
|
|
||||||
var actualException = await Assert.ThrowsAsync<Exception>(() =>
|
var actualException = await Assert.ThrowsAsync<Exception>(() =>
|
||||||
sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
|
sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
|
||||||
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, null));
|
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, false, null));
|
||||||
Assert.Same(expectedException, actualException);
|
Assert.Same(expectedException, actualException);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)
|
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)
|
||||||
@ -244,9 +244,9 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
|||||||
|
|
||||||
var actual = await Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
|
var actual = await Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
|
||||||
await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
|
await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
|
||||||
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, notes: null));
|
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, null));
|
||||||
|
|
||||||
Assert.Equal("You do not have permissions to send sponsorships on behalf of the organization.", actual.Message);
|
Assert.Equal("You do not have permissions to send sponsorships on behalf of the organization", actual.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -278,9 +278,9 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
|||||||
|
|
||||||
var actual = await Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
|
var actual = await Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
|
||||||
await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
|
await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
|
||||||
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, notes: null));
|
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, null));
|
||||||
|
|
||||||
Assert.Equal("You do not have permissions to send sponsorships on behalf of the organization.", actual.Message);
|
Assert.Equal("You do not have permissions to send sponsorships on behalf of the organization", actual.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -312,7 +312,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
var actual = await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
|
var actual = await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
|
||||||
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, notes);
|
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, notes);
|
||||||
|
|
||||||
|
|
||||||
var expectedSponsorship = new OrganizationSponsorship
|
var expectedSponsorship = new OrganizationSponsorship
|
||||||
@ -330,6 +330,6 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
|||||||
Assert.True(SponsorshipValidator(expectedSponsorship, actual));
|
Assert.True(SponsorshipValidator(expectedSponsorship, actual));
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)
|
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)
|
||||||
.UpsertAsync(Arg.Is<OrganizationSponsorship>(s => SponsorshipValidator(s, expectedSponsorship)));
|
.CreateAsync(Arg.Is<OrganizationSponsorship>(s => SponsorshipValidator(s, expectedSponsorship)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
-- Update OrganizationUser_ReadOccupiedSeatCountByOrganizationId to include admin-initiated sponsorships
|
||||||
|
-- Based on https://bitwarden.atlassian.net/browse/PM-17772
|
||||||
|
IF OBJECT_ID('[dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
(
|
||||||
|
-- Count organization users
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM [dbo].[OrganizationUserView]
|
||||||
|
WHERE OrganizationId = @OrganizationId
|
||||||
|
AND Status >= 0 --Invited
|
||||||
|
) +
|
||||||
|
(
|
||||||
|
-- Count admin-initiated sponsorships towards the seat count
|
||||||
|
-- Introduced in https://bitwarden.atlassian.net/browse/PM-17772
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM [dbo].[OrganizationSponsorship]
|
||||||
|
WHERE SponsoringOrganizationId = @OrganizationId
|
||||||
|
AND IsAdminInitiated = 1
|
||||||
|
)
|
||||||
|
END
|
||||||
|
GO
|
Loading…
x
Reference in New Issue
Block a user