1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-25 12:45:18 +01:00

[AC-244] Consider a user's email as verified when they accept an organization invitation via the email link (#3199)

* [AC-244] Saving User.EmailVerified = true when accepting organization invite

* [AC-244] Added unit tests

* [AC-244] Added the parameter 'verifyEmail' to OrganizationService.AcceptUserAsync

* [AC-244] Refactored unit tests

* [AC-244] Fixed failing unit tests

* [AC-244] Marking email as verified only in the endpoint for accepting an invite with a token

* Update src/Core/Services/IOrganizationService.cs

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* [AC-244] Marking email as verified only if it was not

* [AC-244] Updated unit test to have the user's email unverified at the start

* [AC-244] dotnet format

---------

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Vincent Salucci <vincesalucci21@gmail.com>
This commit is contained in:
Rui Tomé 2023-09-07 14:36:54 +01:00 committed by GitHub
parent 8c75326439
commit 721c18e94a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 81 additions and 1 deletions

View File

@ -39,6 +39,12 @@ public interface IOrganizationService
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups);
Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false);
/// <summary>
/// Moves an OrganizationUser into the Accepted status and marks their email as verified.
/// This method is used where the user has clicked the invitation link sent by email.
/// </summary>
/// <param name="token">The token embedded in the email invitation link</param>
/// <returns>The accepted OrganizationUser.</returns>
Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token, IUserService userService);
Task<OrganizationUser> AcceptUserAsync(string orgIdentifier, User user, IUserService userService);
Task<OrganizationUser> AcceptUserAsync(Guid organizationId, User user, IUserService userService);

View File

@ -1093,7 +1093,15 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException("User email does not match invite.");
}
return await AcceptUserAsync(orgUser, user, userService);
var organizationUser = await AcceptUserAsync(orgUser, user, userService);
if (user.EmailVerified == false)
{
user.EmailVerified = true;
await _userRepository.ReplaceAsync(user);
}
return organizationUser;
}
public async Task<OrganizationUser> AcceptUserAsync(string orgIdentifier, User user, IUserService userService)

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

@ -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;
@ -1835,6 +1836,42 @@ 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,