mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 08:02:49 -05:00
Implemented Custom role and permissions (#1057)
* Implemented Custom role and permissions * Converted permissions columns to a json blob * Code review fixes for Permissions * sql build fix * Update Permissions.cs * formatting * Update IOrganizationService.cs * reworked a conditional * built out tests for relevant organization service methods * removed unused usings * fixed a broken test and a bad empty string init * removed 'Attribute' from some attribute instances
This commit is contained in:
@ -51,7 +51,7 @@ namespace Bit.Core.Test.AutoFixture.CipherFixtures
|
||||
{
|
||||
public InlineKnownUserCipherAutoDataAttribute(string userId, params object[] values) : base(new ICustomization[]
|
||||
{ new SutProviderCustomization(), new UserCipher { UserId = new Guid(userId) } }, values)
|
||||
{ }
|
||||
{ }
|
||||
}
|
||||
|
||||
internal class OrganizationCipherAutoDataAttribute : CustomAutoDataAttribute
|
||||
|
114
test/Core.Test/AutoFixture/OrganizationFixtures.cs
Normal file
114
test/Core.Test/AutoFixture/OrganizationFixtures.cs
Normal file
@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using AutoFixture;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Test.AutoFixture.Attributes;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Test.AutoFixture.OrganizationFixtures
|
||||
{
|
||||
internal class PaidOrganization : ICustomization
|
||||
{
|
||||
public PlanType CheckedPlanType { get; set; }
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
var validUpgradePlans = StaticStore.Plans.Where(p => p.Type != Enums.PlanType.Free && !p.Disabled).Select(p => p.Type).ToList();
|
||||
var lowestActivePaidPlan = validUpgradePlans.First();
|
||||
CheckedPlanType = CheckedPlanType.Equals(Enums.PlanType.Free) ? lowestActivePaidPlan : CheckedPlanType;
|
||||
validUpgradePlans.Remove(lowestActivePaidPlan);
|
||||
fixture.Customize<Organization>(composer => composer
|
||||
.With(o => o.PlanType, CheckedPlanType));
|
||||
fixture.Customize<OrganizationUpgrade>(composer => composer
|
||||
.With(ou => ou.Plan, validUpgradePlans.First()));
|
||||
}
|
||||
}
|
||||
|
||||
internal class FreeOrganizationUpgrade : ICustomization
|
||||
{
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customize<Organization>(composer => composer
|
||||
.With(o => o.PlanType, PlanType.Free));
|
||||
|
||||
var plansToIgnore = new List<PlanType> { PlanType.Free, PlanType.Custom };
|
||||
var validPlans = StaticStore.Plans.Where(p => !plansToIgnore.Contains(p.Type) && !p.Disabled).Select(p => p.Type).ToList();
|
||||
fixture.Customize<OrganizationUpgrade>(composer => composer
|
||||
.With(ou => ou.Plan, validPlans.Last()));
|
||||
fixture.Customize<Organization>(composer => composer
|
||||
.Without(o => o.GatewaySubscriptionId));
|
||||
}
|
||||
}
|
||||
internal class OrganizationInvite : ICustomization
|
||||
{
|
||||
public OrganizationUserType InviteeUserType { get; set; }
|
||||
public OrganizationUserType InvitorUserType { get; set; }
|
||||
public string PermissionsBlob { get; set; }
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
var organizationId = new Guid();
|
||||
PermissionsBlob = PermissionsBlob ?? JsonSerializer.Serialize(new Permissions(), new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
});
|
||||
fixture.Customize<Organization>(composer => composer
|
||||
.With(o => o.Id, organizationId));
|
||||
fixture.Customize<OrganizationUser>(composer => composer
|
||||
.With(ou => ou.OrganizationId, organizationId)
|
||||
.With(ou => ou.Type, InvitorUserType)
|
||||
.With(ou => ou.Permissions, PermissionsBlob));
|
||||
fixture.Customize<OrganizationUserInvite>(composer => composer
|
||||
.With(oi => oi.Type, InviteeUserType));
|
||||
}
|
||||
}
|
||||
|
||||
internal class PaidOrganizationAutoDataAttribute : CustomAutoDataAttribute
|
||||
{
|
||||
public PaidOrganizationAutoDataAttribute(int planType = 0) : base(new SutProviderCustomization(),
|
||||
new PaidOrganization { CheckedPlanType = (PlanType)planType })
|
||||
{ }
|
||||
}
|
||||
|
||||
internal class InlinePaidOrganizationAutoDataAttribute : InlineCustomAutoDataAttribute
|
||||
{
|
||||
public InlinePaidOrganizationAutoDataAttribute(params object[] values) : base(new[] { typeof(SutProviderCustomization),
|
||||
typeof(PaidOrganization) }, values)
|
||||
{ }
|
||||
}
|
||||
|
||||
internal class FreeOrganizationUpgradeAutoDataAttribute : CustomAutoDataAttribute
|
||||
{
|
||||
public FreeOrganizationUpgradeAutoDataAttribute() : base(new SutProviderCustomization(), new FreeOrganizationUpgrade())
|
||||
{ }
|
||||
}
|
||||
|
||||
internal class InlineFreeOrganizationUpgradeAutoDataAttribute : InlineCustomAutoDataAttribute
|
||||
{
|
||||
public InlineFreeOrganizationUpgradeAutoDataAttribute(params object[] values) : base(new[] { typeof(SutProviderCustomization),
|
||||
typeof(FreeOrganizationUpgrade) }, values)
|
||||
{ }
|
||||
}
|
||||
|
||||
internal class OrganizationInviteAutoDataAttribute : CustomAutoDataAttribute
|
||||
{
|
||||
public OrganizationInviteAutoDataAttribute(int inviteeUserType = 0, int invitorUserType = 0, string permissionsBlob = null) : base(new SutProviderCustomization(),
|
||||
new OrganizationInvite
|
||||
{
|
||||
InviteeUserType = (OrganizationUserType)inviteeUserType,
|
||||
InvitorUserType = (OrganizationUserType)invitorUserType,
|
||||
PermissionsBlob = permissionsBlob,
|
||||
})
|
||||
{ }
|
||||
}
|
||||
|
||||
internal class InlineOrganizationInviteAutoDataAttribute : InlineCustomAutoDataAttribute
|
||||
{
|
||||
public InlineOrganizationInviteAutoDataAttribute(params object[] values) : base(new[] { typeof(SutProviderCustomization),
|
||||
typeof(OrganizationInvite) }, values)
|
||||
{ }
|
||||
}
|
||||
}
|
@ -3,11 +3,18 @@ using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using Bit.Core.Test.AutoFixture;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Test.AutoFixture.Attributes;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Bit.Core.Test.Services
|
||||
{
|
||||
@ -138,5 +145,216 @@ namespace Bit.Core.Test.Services
|
||||
await orgUserRepo.Received(1).UpsertAsync(Arg.Any<OrganizationUser>());
|
||||
await orgUserRepo.Received(2).CreateAsync(Arg.Any<OrganizationUser>());
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task UpgradePlan_OrganizationIsNull_Throws(Guid organizationId, OrganizationUpgrade upgrade,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(Task.FromResult<Organization>(null));
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => sutProvider.Sut.UpgradePlanAsync(organizationId, upgrade));
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task UpgradePlan_GatewayCustomIdIsNull_Throws(Organization organization, OrganizationUpgrade upgrade,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
organization.GatewayCustomerId = string.Empty;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
|
||||
Assert.Contains("no payment method", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task UpgradePlan_AlreadyInPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
upgrade.Plan = organization.PlanType;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
|
||||
Assert.Contains("already on this plan", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, PaidOrganizationAutoData]
|
||||
public async Task UpgradePlan_UpgradeFromPaidPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
|
||||
Assert.Contains("can only upgrade", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[FreeOrganizationUpgradeAutoData]
|
||||
public async Task UpgradePlan_Passes(Organization organization, OrganizationUpgrade upgrade,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(organization);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationInviteAutoData]
|
||||
public async Task InviteUser_NoEmails_Throws(Organization organization, OrganizationUser invitor,
|
||||
OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
invite.Emails = null;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, null, invite));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationInviteAutoData(
|
||||
inviteeUserType: (int)OrganizationUserType.Owner,
|
||||
invitorUserType: (int)OrganizationUserType.Admin
|
||||
)]
|
||||
public async Task InviteUser_NonOwnerConfiguringOwner_Throws(Organization organization, OrganizationUserInvite invite,
|
||||
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
|
||||
organizationUserRepository.GetManyByUserAsync(invitor.Id).Returns(new List<OrganizationUser> { invitor });
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, null, invite));
|
||||
Assert.Contains("only an owner", exception.Message.ToLowerInvariant());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationInviteAutoData(
|
||||
inviteeUserType: (int)OrganizationUserType.Custom,
|
||||
invitorUserType: (int)OrganizationUserType.Admin
|
||||
)]
|
||||
public async Task InviteUser_NonAdminConfiguringAdmin_Throws(Organization organization, OrganizationUserInvite invite,
|
||||
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
|
||||
organizationUserRepository.GetManyByUserAsync(invitor.Id).Returns(new List<OrganizationUser> { invitor });
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, null, invite));
|
||||
Assert.Contains("only owners and admins", exception.Message.ToLowerInvariant());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationInviteAutoData(
|
||||
inviteeUserType: (int)OrganizationUserType.Manager,
|
||||
invitorUserType: (int)OrganizationUserType.Custom
|
||||
)]
|
||||
public async Task InviteUser_CustomUserWithoutManageUsersConfiguringUser_Throws(Organization organization, OrganizationUserInvite invite,
|
||||
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = false },
|
||||
new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
});
|
||||
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
|
||||
organizationUserRepository.GetManyByUserAsync(invitor.UserId.Value).Returns(new List<OrganizationUser> { invitor });
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, null, invite));
|
||||
Assert.Contains("account does not have permission", exception.Message.ToLowerInvariant());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationInviteAutoData(
|
||||
inviteeUserType: (int)OrganizationUserType.Admin,
|
||||
invitorUserType: (int)OrganizationUserType.Custom
|
||||
)]
|
||||
public async Task InviteUser_CustomUserConfiguringAdmin_Throws(Organization organization, OrganizationUserInvite invite,
|
||||
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = true },
|
||||
new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
});
|
||||
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
|
||||
organizationUserRepository.GetManyByUserAsync(invitor.UserId.Value).Returns(new List<OrganizationUser> { invitor });
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, null, invite));
|
||||
Assert.Contains("can not manage admins", exception.Message.ToLowerInvariant());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationInviteAutoData(
|
||||
inviteeUserType: (int)OrganizationUserType.User,
|
||||
invitorUserType: (int)OrganizationUserType.Custom
|
||||
)]
|
||||
public async Task InviteUser_Passes(Organization organization, OrganizationUserInvite invite,
|
||||
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = true },
|
||||
new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
});
|
||||
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var eventService = sutProvider.GetDependency<IEventService>();
|
||||
|
||||
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
|
||||
organizationUserRepository.GetManyByUserAsync(invitor.UserId.Value).Returns(new List<OrganizationUser> { invitor });
|
||||
|
||||
await sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, null, invite);
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task SaveUser_NoUserId_Throws(OrganizationUser user, Guid? savingUserId,
|
||||
IEnumerable<SelectionReadOnly> collections, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
user.Id = default(Guid);
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveUserAsync(user, savingUserId, collections));
|
||||
Assert.Contains("invite the user first", exception.Message.ToLowerInvariant());
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task SaveUser_NoChangeToData_Throws(OrganizationUser user, Guid? savingUserId,
|
||||
IEnumerable<SelectionReadOnly> collections, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
organizationUserRepository.GetByIdAsync(user.Id).Returns(user);
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveUserAsync(user, savingUserId, collections));
|
||||
Assert.Contains("make changes before saving", exception.Message.ToLowerInvariant());
|
||||
}
|
||||
|
||||
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
|
||||
public async Task SaveUser_Passes(OrganizationUser oldUserData, OrganizationUser newUserData,
|
||||
IEnumerable<SelectionReadOnly> collections, OrganizationUser savingUser, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
newUserData.Id = oldUserData.Id;
|
||||
newUserData.UserId = oldUserData.UserId;
|
||||
newUserData.OrganizationId = savingUser.OrganizationId = oldUserData.OrganizationId;
|
||||
savingUser.Type = OrganizationUserType.Owner;
|
||||
organizationUserRepository.GetByIdAsync(oldUserData.Id).Returns(oldUserData);
|
||||
organizationUserRepository.GetManyByUserAsync(savingUser.UserId.Value).Returns(new List<OrganizationUser> { savingUser });
|
||||
|
||||
await sutProvider.Sut.SaveUserAsync(newUserData, savingUser.UserId, collections);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user