1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-22 20:11:04 -05:00
This commit is contained in:
Jonas Hendrickx 2025-03-11 12:51:42 +01:00
parent 224ef1272e
commit 08924e10d2
10 changed files with 124 additions and 16 deletions

View File

@ -77,9 +77,10 @@ public class OrganizationSponsorshipsController : Controller
{ {
var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgId); var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgId);
var targetUser = model.SponsoringUserId ?? _currentContext.UserId!.Value;
var sponsorship = await _createSponsorshipCommand.CreateSponsorshipAsync( var sponsorship = await _createSponsorshipCommand.CreateSponsorshipAsync(
sponsoringOrg, sponsoringOrg,
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default), await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, targetUser),
model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName); model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName);
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name); await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name);
} }

View File

@ -16,4 +16,10 @@ public class OrganizationSponsorshipCreateRequestModel
[StringLength(256)] [StringLength(256)]
public string FriendlyName { get; set; } public string FriendlyName { get; set; }
/// <summary>
/// (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; }
} }

View File

@ -114,6 +114,11 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
/// </summary> /// </summary>
public bool UseRiskInsights { get; set; } public bool UseRiskInsights { get; set; }
/// <summary>
/// If set to true, admins can initiate organization-issued sponsorships.
/// </summary>
public bool UseAdminSponsoredFamilies { get; set; }
public void SetNewId() public void SetNewId()
{ {
if (Id == default(Guid)) if (Id == default(Guid))

View File

@ -26,6 +26,7 @@ public class OrganizationAbility
LimitItemDeletion = organization.LimitItemDeletion; LimitItemDeletion = organization.LimitItemDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UseRiskInsights = organization.UseRiskInsights; UseRiskInsights = organization.UseRiskInsights;
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
} }
public Guid Id { get; set; } public Guid Id { get; set; }
@ -45,4 +46,5 @@ public class OrganizationAbility
public bool LimitItemDeletion { get; set; } public bool LimitItemDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
public bool UseRiskInsights { get; set; } public bool UseRiskInsights { get; set; }
public bool UseAdminSponsoredFamilies { get; set; }
} }

View File

@ -20,6 +20,7 @@ public class OrganizationSponsorship : ITableObject<Guid>
public DateTime? LastSyncDate { get; set; } public DateTime? LastSyncDate { get; set; }
public DateTime? ValidUntil { get; set; } public DateTime? ValidUntil { get; set; }
public bool ToDelete { get; set; } public bool ToDelete { get; set; }
public bool IsAdminInitiated { get; set; }
public void SetNewId() public void SetNewId()
{ {

View File

@ -16,6 +16,7 @@ public class OrganizationSponsorshipData
LastSyncDate = sponsorship.LastSyncDate; LastSyncDate = sponsorship.LastSyncDate;
ValidUntil = sponsorship.ValidUntil; ValidUntil = sponsorship.ValidUntil;
ToDelete = sponsorship.ToDelete; ToDelete = sponsorship.ToDelete;
IsAdminInitiated = sponsorship.IsAdminInitiated;
} }
public Guid SponsoringOrganizationUserId { get; set; } public Guid SponsoringOrganizationUserId { get; set; }
public Guid? SponsoredOrganizationId { get; set; } public Guid? SponsoredOrganizationId { get; set; }
@ -25,6 +26,7 @@ public class OrganizationSponsorshipData
public DateTime? LastSyncDate { get; set; } public DateTime? LastSyncDate { get; set; }
public DateTime? ValidUntil { get; set; } public DateTime? ValidUntil { get; set; }
public bool ToDelete { get; set; } public bool ToDelete { get; set; }
public bool IsAdminInitiated { get; set; }
public bool CloudSponsorshipRemoved { get; set; } public bool CloudSponsorshipRemoved { get; set; }
} }

View File

@ -1,5 +1,6 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -10,22 +11,16 @@ using Bit.Core.Utilities;
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise; namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;
public class CreateSponsorshipCommand : ICreateSponsorshipCommand public class CreateSponsorshipCommand(
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IUserService userService,
ICurrentContext currentContext)
: ICreateSponsorshipCommand
{ {
private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;
private readonly IUserService _userService;
public CreateSponsorshipCommand(IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IUserService userService)
{
_organizationSponsorshipRepository = organizationSponsorshipRepository;
_userService = userService;
}
public async Task<OrganizationSponsorship> CreateSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, public async Task<OrganizationSponsorship> CreateSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser,
PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName) PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName)
{ {
var sponsoringUser = await _userService.GetUserByIdAsync(sponsoringOrgUser.UserId.Value); var sponsoringUser = await userService.GetUserByIdAsync(sponsoringOrgUser.UserId.Value);
if (sponsoringUser == null || string.Equals(sponsoringUser.Email, sponsoredEmail, System.StringComparison.InvariantCultureIgnoreCase)) if (sponsoringUser == null || string.Equals(sponsoringUser.Email, sponsoredEmail, System.StringComparison.InvariantCultureIgnoreCase))
{ {
throw new BadRequestException("Cannot offer a Families Organization Sponsorship to yourself. Choose a different email."); throw new BadRequestException("Cannot offer a Families Organization Sponsorship to yourself. Choose a different email.");
@ -45,7 +40,24 @@ public class CreateSponsorshipCommand : ICreateSponsorshipCommand
throw new BadRequestException("Only confirmed users can sponsor other organizations."); throw new BadRequestException("Only confirmed users can sponsor other organizations.");
} }
var existingOrgSponsorship = await _organizationSponsorshipRepository var isAdminInitiated = false;
if (currentContext.UserId != sponsoringOrgUser.UserId)
{
var organization = currentContext.Organizations.First(x => x.Id == sponsoringOrg.Id);
OrganizationUserType[] allowedUserTypes =
[
OrganizationUserType.Admin,
OrganizationUserType.Owner,
OrganizationUserType.Custom
];
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.");
}
isAdminInitiated = true;
}
var existingOrgSponsorship = await organizationSponsorshipRepository
.GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id); .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id);
if (existingOrgSponsorship?.SponsoredOrganizationId != null) if (existingOrgSponsorship?.SponsoredOrganizationId != null)
{ {
@ -59,6 +71,7 @@ public class CreateSponsorshipCommand : ICreateSponsorshipCommand
FriendlyName = friendlyName, FriendlyName = friendlyName,
OfferedToEmail = sponsoredEmail, OfferedToEmail = sponsoredEmail,
PlanSponsorshipType = sponsorshipType, PlanSponsorshipType = sponsorshipType,
IsAdminInitiated = isAdminInitiated
}; };
if (existingOrgSponsorship != null) if (existingOrgSponsorship != null)
@ -69,14 +82,14 @@ public class CreateSponsorshipCommand : ICreateSponsorshipCommand
try try
{ {
await _organizationSponsorshipRepository.UpsertAsync(sponsorship); await organizationSponsorshipRepository.UpsertAsync(sponsorship);
return sponsorship; return sponsorship;
} }
catch catch
{ {
if (sponsorship.Id != default) if (sponsorship.Id != default)
{ {
await _organizationSponsorshipRepository.DeleteAsync(sponsorship); await organizationSponsorshipRepository.DeleteAsync(sponsorship);
} }
throw; throw;
} }

View File

@ -56,6 +56,7 @@ CREATE TABLE [dbo].[Organization] (
[LimitItemDeletion] BIT NOT NULL CONSTRAINT [DF_Organization_LimitItemDeletion] DEFAULT (0), [LimitItemDeletion] BIT NOT NULL CONSTRAINT [DF_Organization_LimitItemDeletion] DEFAULT (0),
[AllowAdminAccessToAllCollectionItems] BIT NOT NULL CONSTRAINT [DF_Organization_AllowAdminAccessToAllCollectionItems] DEFAULT (0), [AllowAdminAccessToAllCollectionItems] BIT NOT NULL CONSTRAINT [DF_Organization_AllowAdminAccessToAllCollectionItems] DEFAULT (0),
[UseRiskInsights] BIT NOT NULL CONSTRAINT [DF_Organization_UseRiskInsights] DEFAULT (0), [UseRiskInsights] BIT NOT NULL CONSTRAINT [DF_Organization_UseRiskInsights] DEFAULT (0),
[UseAdminSponsoredFamilies] BIT NOT NULL CONSTRAINT [DF_Organization_UseAdminSponsoredFamilies] DEFAULT (0),
CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC) CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC)
); );

View File

@ -9,6 +9,7 @@ CREATE TABLE [dbo].[OrganizationSponsorship] (
[ToDelete] BIT DEFAULT (0) NOT NULL, [ToDelete] BIT DEFAULT (0) NOT NULL,
[LastSyncDate] DATETIME2 (7) NULL, [LastSyncDate] DATETIME2 (7) NULL,
[ValidUntil] DATETIME2 (7) NULL, [ValidUntil] DATETIME2 (7) NULL,
[IsAdminInitiated] BIT NOT NULL CONSTRAINT [DF_OrganizationSponsorship_IsAdminInitiated] DEFAULT (0),
CONSTRAINT [PK_OrganizationSponsorship] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [PK_OrganizationSponsorship] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_OrganizationSponsorship_SponsoringOrg] FOREIGN KEY ([SponsoringOrganizationId]) REFERENCES [dbo].[Organization] ([Id]), CONSTRAINT [FK_OrganizationSponsorship_SponsoringOrg] FOREIGN KEY ([SponsoringOrganizationId]) REFERENCES [dbo].[Organization] ([Id]),
CONSTRAINT [FK_OrganizationSponsorship_SponsoredOrg] FOREIGN KEY ([SponsoredOrganizationId]) REFERENCES [dbo].[Organization] ([Id]), CONSTRAINT [FK_OrganizationSponsorship_SponsoredOrg] FOREIGN KEY ([SponsoredOrganizationId]) REFERENCES [dbo].[Organization] ([Id]),

View File

@ -1,8 +1,10 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -110,6 +112,8 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
sutProvider.GetDependency<IOrganizationSponsorshipRepository>() sutProvider.GetDependency<IOrganizationSponsorshipRepository>()
.GetBySponsoringOrganizationUserIdAsync(orgUser.Id).Returns(sponsorship); .GetBySponsoringOrganizationUserIdAsync(orgUser.Id).Returns(sponsorship);
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, default, default)); sutProvider.Sut.CreateSponsorshipAsync(org, orgUser, sponsorship.PlanSponsorshipType.Value, default, default));
@ -132,6 +136,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
var sponsorship = callInfo.Arg<OrganizationSponsorship>(); var sponsorship = callInfo.Arg<OrganizationSponsorship>();
sponsorship.Id = sponsorshipId; sponsorship.Id = sponsorshipId;
}); });
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(sponsoringOrgUser.UserId.Value);
await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
@ -168,6 +173,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
createdSponsorship.Id = Guid.NewGuid(); createdSponsorship.Id = Guid.NewGuid();
return expectedException; return expectedException;
}); });
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(sponsoringOrgUser.UserId.Value);
var actualException = await Assert.ThrowsAsync<Exception>(() => var actualException = await Assert.ThrowsAsync<Exception>(() =>
sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
@ -177,4 +183,74 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1) await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)
.DeleteAsync(createdSponsorship); .DeleteAsync(createdSponsorship);
} }
[Theory]
[BitAutoData]
public async Task CreateSponsorship_MissingManageUsersPermission_ThrowsUnauthorizedException(
Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, User user, string sponsoredEmail,
string friendlyName, Guid sponsorshipId, Guid currentUserId, SutProvider<CreateSponsorshipCommand> sutProvider)
{
sponsoringOrg.PlanType = PlanType.EnterpriseAnnually;
sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed;
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(sponsoringOrgUser.UserId.Value).Returns(user);
sutProvider.GetDependency<IOrganizationSponsorshipRepository>().WhenForAnyArgs(x => x.UpsertAsync(default)).Do(callInfo =>
{
var sponsorship = callInfo.Arg<OrganizationSponsorship>();
sponsorship.Id = sponsorshipId;
});
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(currentUserId);
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns([
new()
{
Id = sponsoringOrg.Id,
Permissions = new Permissions(),
Type = OrganizationUserType.Admin
}
]);
var actual = await Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName));
Assert.Equal("You do not have permissions to send sponsorships on behalf of the organization.", actual.Message);
}
[Theory]
[BitAutoData(OrganizationUserType.User)]
public async Task CreateSponsorship_InvalidUserType_ThrowsUnauthorizedException(
OrganizationUserType organizationUserType,
Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, User user, string sponsoredEmail,
string friendlyName, Guid sponsorshipId, Guid currentUserId, SutProvider<CreateSponsorshipCommand> sutProvider)
{
sponsoringOrg.PlanType = PlanType.EnterpriseAnnually;
sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed;
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(sponsoringOrgUser.UserId.Value).Returns(user);
sutProvider.GetDependency<IOrganizationSponsorshipRepository>().WhenForAnyArgs(x => x.UpsertAsync(default)).Do(callInfo =>
{
var sponsorship = callInfo.Arg<OrganizationSponsorship>();
sponsorship.Id = sponsorshipId;
});
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(currentUserId);
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns([
new()
{
Id = sponsoringOrg.Id,
Permissions = new Permissions
{
ManageUsers = true,
},
Type = organizationUserType
}
]);
var actual = await Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName));
Assert.Equal("You do not have permissions to send sponsorships on behalf of the organization.", actual.Message);
}
} }