1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-04 01:22:50 -05:00

Merge remote-tracking branch 'origin/master' into ac/ac-1638/disallow-secrets-manager-for-msp-managed-organizations

This commit is contained in:
Thomas Rittson
2023-09-25 14:29:59 +10:00
104 changed files with 9865 additions and 2034 deletions

View File

@ -0,0 +1,173 @@
using AutoFixture.Xunit2;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Entities;
using Bit.Core.Tokens;
using Xunit;
namespace Bit.Core.Test.Auth.Models.Business.Tokenables;
// Note: these test names follow MethodName_StateUnderTest_ExpectedBehavior pattern.
public class SsoEmail2faSessionTokenableTests
{
// Allow a small tolerance for possible execution delays or clock precision to avoid flaky tests.
private static readonly TimeSpan _timeTolerance = TimeSpan.FromMilliseconds(10);
/// <summary>
/// Tests the default constructor behavior when passed a null user.
/// </summary>
[Fact]
public void Constructor_NullUser_PropertiesSetToDefault()
{
var token = new SsoEmail2faSessionTokenable(null);
Assert.Equal(default, token.Id);
Assert.Equal(default, token.Email);
}
/// <summary>
/// Tests that when a valid user is provided to the constructor, the resulting token properties match the user.
/// </summary>
[Theory, AutoData]
public void Constructor_ValidUser_PropertiesSetFromUser(User user)
{
var token = new SsoEmail2faSessionTokenable(user);
Assert.Equal(user.Id, token.Id);
Assert.Equal(user.Email, token.Email);
}
/// <summary>
/// Tests the default expiration behavior immediately after initialization.
/// </summary>
[Fact]
public void Constructor_AfterInitialization_ExpirationSetToExpectedDuration()
{
var token = new SsoEmail2faSessionTokenable();
var expectedExpiration = DateTime.UtcNow + SsoEmail2faSessionTokenable.GetTokenLifetime();
Assert.True(expectedExpiration - token.ExpirationDate < _timeTolerance);
}
/// <summary>
/// Tests that a custom expiration date is preserved after token initialization.
/// </summary>
[Fact]
public void Constructor_CustomExpirationDate_ExpirationMatchesProvidedValue()
{
var customExpiration = DateTime.UtcNow.AddHours(3);
var token = new SsoEmail2faSessionTokenable
{
ExpirationDate = customExpiration
};
Assert.True((customExpiration - token.ExpirationDate).Duration() < _timeTolerance);
}
/// <summary>
/// Tests the validity of a token initialized with a null user.
/// </summary>
[Fact]
public void Valid_NullUser_ReturnsFalse()
{
var token = new SsoEmail2faSessionTokenable(null);
Assert.False(token.Valid);
}
/// <summary>
/// Tests the validity of a token with a non-matching identifier.
/// </summary>
[Theory, AutoData]
public void Valid_WrongIdentifier_ReturnsFalse(User user)
{
var token = new SsoEmail2faSessionTokenable(user)
{
Identifier = "not correct"
};
Assert.False(token.Valid);
}
/// <summary>
/// Tests the token validity when user ID is null.
/// </summary>
[Theory, AutoData]
public void TokenIsValid_NullUserId_ReturnsFalse(User user)
{
user.Id = default; // Guid.Empty
var token = new SsoEmail2faSessionTokenable(user);
Assert.False(token.TokenIsValid(user));
}
/// <summary>
/// Tests the token validity when user's email is null.
/// </summary>
[Theory, AutoData]
public void TokenIsValid_NullEmail_ReturnsFalse(User user)
{
user.Email = null;
var token = new SsoEmail2faSessionTokenable(user);
Assert.False(token.TokenIsValid(user));
}
/// <summary>
/// Tests the token validity when user ID and email match the token properties.
/// </summary>
[Theory, AutoData]
public void TokenIsValid_MatchingUserIdAndEmail_ReturnsTrue(User user)
{
var token = new SsoEmail2faSessionTokenable(user);
Assert.True(token.TokenIsValid(user));
}
/// <summary>
/// Ensures that the token is invalid when the provided user's ID doesn't match the token's ID.
/// </summary>
[Theory, AutoData]
public void TokenIsValid_WrongUserId_ReturnsFalse(User user)
{
// Given a token initialized with a user's details
var token = new SsoEmail2faSessionTokenable(user);
// modify the user's ID
user.Id = Guid.NewGuid();
// Then the token should be considered invalid
Assert.False(token.TokenIsValid(user));
}
/// <summary>
/// Ensures that the token is invalid when the provided user's email doesn't match the token's email.
/// </summary>
[Theory, AutoData]
public void TokenIsValid_WrongEmail_ReturnsFalse(User user)
{
// Given a token initialized with a user's details
var token = new SsoEmail2faSessionTokenable(user);
// modify the user's email
user.Email = "nonMatchingEmail@example.com";
// Then the token should be considered invalid
Assert.False(token.TokenIsValid(user));
}
/// <summary>
/// Tests the deserialization of a token to ensure that the expiration date is preserved.
/// </summary>
[Theory, AutoData]
public void FromToken_SerializedToken_PreservesExpirationDate(User user)
{
var expectedDateTime = DateTime.UtcNow.AddHours(-5);
var token = new SsoEmail2faSessionTokenable(user)
{
ExpirationDate = expectedDateTime
};
var result = Tokenable.FromToken<SsoEmail2faSessionTokenable>(token.ToToken());
Assert.Equal(expectedDateTime, result.ExpirationDate, precision: _timeTolerance);
}
}

View File

@ -142,15 +142,19 @@ public class AuthRequestServiceTests
}
[Theory, BitAutoData]
public async Task CreateAuthRequestAsync_NoUser_ThrowsNotFound(
public async Task CreateAuthRequestAsync_NoUser_ThrowsBadRequest(
SutProvider<AuthRequestService> sutProvider,
AuthRequestCreateRequestModel createModel)
{
sutProvider.GetDependency<ICurrentContext>()
.DeviceType
.Returns(DeviceType.Android);
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(createModel.Email)
.Returns((User?)null);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAuthRequestAsync(createModel));
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAuthRequestAsync(createModel));
}
[Theory, BitAutoData]
@ -253,7 +257,7 @@ public class AuthRequestServiceTests
/// <summary>
/// Story: If a user happens to exist to more than one organization, we will send the device approval request to
/// each of them.
/// each of them.
/// </summary>
[Theory, BitAutoData]
public async Task CreateAuthRequestAsync_AdminApproval_CreatesForEachOrganization(
@ -627,8 +631,8 @@ public class AuthRequestServiceTests
}
/// <summary>
/// Story: An admin approves a request for one of their org users. For auditing purposes we need to
/// log an event that correlates the action for who the request was approved for. On approval we also need to
/// Story: An admin approves a request for one of their org users. For auditing purposes we need to
/// log an event that correlates the action for who the request was approved for. On approval we also need to
/// push the notification to the user.
/// </summary>
[Theory, BitAutoData]

View File

@ -10,6 +10,7 @@ using Bit.Core.Models.Data;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.DataProtection;
namespace Bit.Core.Test.AutoFixture.OrganizationFixtures;
@ -187,3 +188,31 @@ internal class SecretsManagerOrganizationCustomizeAttribute : BitCustomizeAttrib
public override ICustomization GetCustomization() =>
new SecretsManagerOrganizationCustomization();
}
internal class EphemeralDataProtectionCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customizations.Add(new EphemeralDataProtectionProviderBuilder());
}
private class EphemeralDataProtectionProviderBuilder : ISpecimenBuilder
{
public object Create(object request, ISpecimenContext context)
{
var type = request as Type;
if (type == null || type != typeof(IDataProtectionProvider))
{
return new NoSpecimen();
}
return new EphemeralDataProtectionProvider();
}
}
}
internal class EphemeralDataProtectionAutoDataAttribute : CustomAutoDataAttribute
{
public EphemeralDataProtectionAutoDataAttribute() : base(new SutProviderCustomization(), new EphemeralDataProtectionCustomization())
{ }
}

View File

@ -0,0 +1,50 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.OrganizationFeatures.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.OrganizationFeatures.OrganizationUsers;
[SutProviderCustomize]
public class UpdateOrganizationUserGroupsCommandTests
{
[Theory, BitAutoData]
public async Task UpdateUserGroups_Passes(
OrganizationUser organizationUser,
IEnumerable<Guid> groupIds,
SutProvider<UpdateOrganizationUserGroupsCommand> sutProvider)
{
await sutProvider.Sut.UpdateUserGroupsAsync(organizationUser, groupIds, null);
await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs()
.ValidateOrganizationUserUpdatePermissions(default, default, default, default);
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
.UpdateGroupsAsync(organizationUser.Id, groupIds);
await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UpdatedGroups);
}
[Theory, BitAutoData]
public async Task UpdateUserGroups_WithSavingUserId_Passes(
OrganizationUser organizationUser,
IEnumerable<Guid> groupIds,
Guid savingUserId,
SutProvider<UpdateOrganizationUserGroupsCommand> sutProvider)
{
organizationUser.Permissions = null;
await sutProvider.Sut.UpdateUserGroupsAsync(organizationUser, groupIds, savingUserId);
await sutProvider.GetDependency<IOrganizationService>().Received(1)
.ValidateOrganizationUserUpdatePermissions(organizationUser.OrganizationId, organizationUser.Type, null, organizationUser.GetPermissions());
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
.UpdateGroupsAsync(organizationUser.Id, groupIds);
await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UpdatedGroups);
}
}

View File

@ -20,14 +20,6 @@ public class LaunchDarklyFeatureServiceTests
.Create();
}
[Fact]
public void Offline_WhenSelfHost()
{
var sutProvider = GetSutProvider(new Core.Settings.GlobalSettings() { SelfHosted = true });
Assert.False(sutProvider.Sut.IsOnline());
}
[Theory, BitAutoData]
public void DefaultFeatureValue_WhenSelfHost(string key)
{

View File

@ -28,6 +28,7 @@ using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.DataProtection;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
@ -658,11 +659,10 @@ public class OrganizationServiceTests
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, EventSystemUser, DateTime?)>>());
}
[Theory, BitAutoData]
[Theory, BitAutoData, OrganizationInviteCustomize]
public async Task InviteUser_WithSecretsManager_Passes(Organization organization,
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites,
[OrganizationUser(type: OrganizationUserType.Owner, status: OrganizationUserStatusType.Confirmed)] OrganizationUser savingUser,
SutProvider<OrganizationService> sutProvider)
OrganizationUser savingUser, SutProvider<OrganizationService> sutProvider)
{
organization.PlanType = PlanType.EnterpriseAnnually;
InviteUserHelper_ArrangeValidPermissions(organization, savingUser, sutProvider);
@ -670,6 +670,10 @@ public class OrganizationServiceTests
// Set up some invites to grant access to SM
invites.First().invite.AccessSecretsManager = true;
var invitedSmUsers = invites.First().invite.Emails.Count();
foreach (var (invite, externalId) in invites.Skip(1))
{
invite.AccessSecretsManager = false;
}
// Assume we need to add seats for all invited SM users
sutProvider.GetDependency<ICountNewSmSeatsRequiredQuery>()
@ -685,11 +689,10 @@ public class OrganizationServiceTests
!update.MaxAutoscaleSmSeatsChanged));
}
[Theory, BitAutoData]
[Theory, BitAutoData, OrganizationInviteCustomize]
public async Task InviteUser_WithSecretsManager_WhenErrorIsThrown_RevertsAutoscaling(Organization organization,
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites,
[OrganizationUser(type: OrganizationUserType.Owner, status: OrganizationUserStatusType.Confirmed)] OrganizationUser savingUser,
SutProvider<OrganizationService> sutProvider)
OrganizationUser savingUser, SutProvider<OrganizationService> sutProvider)
{
var initialSmSeats = organization.SmSeats;
InviteUserHelper_ArrangeValidPermissions(organization, savingUser, sutProvider);
@ -697,6 +700,10 @@ public class OrganizationServiceTests
// Set up some invites to grant access to SM
invites.First().invite.AccessSecretsManager = true;
var invitedSmUsers = invites.First().invite.Emails.Count();
foreach (var (invite, externalId) in invites.Skip(1))
{
invite.AccessSecretsManager = false;
}
// Assume we need to add seats for all invited SM users
sutProvider.GetDependency<ICountNewSmSeatsRequiredQuery>()
@ -741,13 +748,9 @@ public class OrganizationServiceTests
private void InviteUserHelper_ArrangeValidPermissions(Organization organization, OrganizationUser savingUser,
SutProvider<OrganizationService> sutProvider)
{
savingUser.OrganizationId = organization.Id;
organization.UseCustomPermissions = true;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByOrganizationAsync(savingUser.OrganizationId, OrganizationUserType.Owner)
.Returns(new List<OrganizationUser> { savingUser });
}
[Theory, BitAutoData]
@ -1848,4 +1851,110 @@ public class OrganizationServiceTests
sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup);
}
[Theory]
[EphemeralDataProtectionAutoData]
public async Task AcceptUserAsync_Success([OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser organizationUser,
User user, SutProvider<OrganizationService> sutProvider)
{
var token = SetupAcceptUserAsyncTest(sutProvider, user, organizationUser);
var userService = Substitute.For<IUserService>();
await sutProvider.Sut.AcceptUserAsync(organizationUser.Id, user, token, userService);
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).ReplaceAsync(
Arg.Is<OrganizationUser>(ou => ou.Id == organizationUser.Id && ou.Status == OrganizationUserStatusType.Accepted));
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(
Arg.Is<User>(u => u.Id == user.Id && u.Email == user.Email && user.EmailVerified == true));
}
private string SetupAcceptUserAsyncTest(SutProvider<OrganizationService> sutProvider, User user,
OrganizationUser organizationUser)
{
user.Email = organizationUser.Email;
user.EmailVerified = false;
var dataProtector = sutProvider.GetDependency<IDataProtectionProvider>()
.CreateProtector("OrganizationServiceDataProtector");
// Token matching the format used in OrganizationService.InviteUserAsync
var token = dataProtector.Protect(
$"OrganizationUserInvite {organizationUser.Id} {organizationUser.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
sutProvider.GetDependency<IGlobalSettings>().OrganizationInviteExpirationHours.Returns(24);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
return token;
}
[Theory]
[OrganizationInviteCustomize(
InviteeUserType = OrganizationUserType.Owner,
InvitorUserType = OrganizationUserType.Admin
), BitAutoData]
public async Task ValidateOrganizationUserUpdatePermissions_WithAdminAddingOwner_Throws(
Guid organizationId,
OrganizationUserInvite organizationUserInvite,
SutProvider<OrganizationService> sutProvider)
{
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(organizationId, organizationUserInvite.Type.Value, null, organizationUserInvite.Permissions));
Assert.Contains("only an owner can configure another owner's account.", exception.Message.ToLowerInvariant());
}
[Theory]
[OrganizationInviteCustomize(
InviteeUserType = OrganizationUserType.Admin,
InvitorUserType = OrganizationUserType.Owner
), BitAutoData]
public async Task ValidateOrganizationUserUpdatePermissions_WithoutManageUsersPermission_Throws(
Guid organizationId,
OrganizationUserInvite organizationUserInvite,
SutProvider<OrganizationService> sutProvider)
{
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(organizationId, organizationUserInvite.Type.Value, null, organizationUserInvite.Permissions));
Assert.Contains("your account does not have permission to manage users.", exception.Message.ToLowerInvariant());
}
[Theory]
[OrganizationInviteCustomize(
InviteeUserType = OrganizationUserType.Admin,
InvitorUserType = OrganizationUserType.Custom
), BitAutoData]
public async Task ValidateOrganizationUserUpdatePermissions_WithCustomAddingAdmin_Throws(
Guid organizationId,
OrganizationUserInvite organizationUserInvite,
SutProvider<OrganizationService> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationId).Returns(true);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(organizationId, organizationUserInvite.Type.Value, null, organizationUserInvite.Permissions));
Assert.Contains("custom users can not manage admins or owners.", exception.Message.ToLowerInvariant());
}
[Theory]
[OrganizationInviteCustomize(
InviteeUserType = OrganizationUserType.Custom,
InvitorUserType = OrganizationUserType.Custom
), BitAutoData]
public async Task ValidateOrganizationUserUpdatePermissions_WithCustomAddingUser_WithoutPermissions_Throws(
Guid organizationId,
OrganizationUserInvite organizationUserInvite,
SutProvider<OrganizationService> sutProvider)
{
var invitePermissions = new Permissions { AccessReports = true };
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().AccessReports(organizationId).Returns(false);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(organizationId, organizationUserInvite.Type.Value, null, invitePermissions));
Assert.Contains("custom users can only grant the same custom permissions that they have.", exception.Message.ToLowerInvariant());
}
}

View File

@ -222,10 +222,11 @@
},
"Braintree": {
"type": "Transitive",
"resolved": "5.12.0",
"contentHash": "bV2tsVIvBQeKwULT4qPZUWhxSr8mFwyAAcvLDvDpCU0cMYPHzGSahha+ghUdgGMb317BqL34/Od59n2s3MkhOQ==",
"resolved": "5.19.0",
"contentHash": "B60wIX54g78nMsy5cJkvSfqs1VasYDXWFZQW0cUQ4QeW8Y5jPyBSaoxHwKC806lXUDaKC8kr5Y7Q6EdsBkPANQ==",
"dependencies": {
"Newtonsoft.Json": "9.0.1",
"Microsoft.CSharp": "4.7.0",
"Newtonsoft.Json": "13.0.1",
"System.Xml.XPath.XmlDocument": "4.3.0"
}
},
@ -2672,7 +2673,7 @@
"dependencies": {
"AutoFixture.AutoNSubstitute": "[4.17.0, )",
"AutoFixture.Xunit2": "[4.17.0, )",
"Core": "[2023.7.2, )",
"Core": "[2023.9.0, )",
"Kralizek.AutoFixture.Extensions.MockHttp": "[1.2.0, )",
"Microsoft.NET.Test.Sdk": "[17.1.0, )",
"NSubstitute": "[4.3.0, )",
@ -2691,7 +2692,7 @@
"Azure.Storage.Blobs": "[12.14.1, )",
"Azure.Storage.Queues": "[12.12.0, )",
"BitPay.Light": "[1.0.1907, )",
"Braintree": "[5.12.0, )",
"Braintree": "[5.19.0, )",
"DnsClient": "[1.7.0, )",
"Fido2.AspNet": "[3.0.1, )",
"Handlebars.Net": "[2.1.2, )",