From 9560a32495d1010b80121d6a57fd36490a089c9f Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:04:05 -0400 Subject: [PATCH] =?UTF-8?q?[SM-1211]=20Adding=20API=20endpoint=20to=20send?= =?UTF-8?q?=20out=20Access=20Request=20for=20SM=20to=20Admins,=20addi?= =?UTF-8?q?=E2=80=A6=20(#4155)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adding API endpoint to send out Access Request for SM to Admins, adding email template * Fixing email template HTML, adding tests * fixing tests * fixing lint * Moving files to proper locations * fixing build error relating to not removing some old code * Updating namespaces and removing unused using statements * Dependency injection fix * Fixing tests and moving them to proper files * lint * format fixes * dotnet format fix * small fixes * removing using directive's that aren't needed * Update bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/PasswordManager/RequestSMAccessCommandTests.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Core/MailTemplates/Handlebars/SecretsManagerAccessRequest.text.hbs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update bitwarden_license/src/Commercial.Core/SecretsManager/Commands/PasswordManager/RequestSMAccessCommand.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Changes requested by Thomas * Lint fixes * Suggested changes from Maceij * Current state of tests * Fixing tests and getting the core.csproj file from main * Reverting csproj file change * Removing usings directory * dotnet format * Fixing test * Update bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/Requests/RequestSMAccessCommandTests.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update test/Api.Test/SecretsManager/Controllers/RequestSMAccessControllerTests.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Thomas requested changes * Fixing 500 error when user name is null * Prettier error message if user sends over an whitespace string * Fixing word wrapping issue in email contents --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --- .../Requests/RequestSMAccessCommand.cs | 34 +++++++ .../SecretsManagerCollectionExtensions.cs | 3 + .../Requests/RequestSMAccessCommandTests.cs | 96 +++++++++++++++++++ .../Controllers/RequestSMAccessController.cs | 55 +++++++++++ .../Request/RequestSMAccessRequestModel.cs | 11 +++ .../SecretsManagerAccessRequest.html.hbs | 27 ++++++ .../SecretsManagerAccessRequest.text.hbs | 17 ++++ .../Interfaces/IRequestSMAccessCommand.cs | 10 ++ .../RequestSecretsManagerAccessViewModel.cs | 10 ++ src/Core/Services/IMailService.cs | 1 + .../Implementations/HandlebarsMailService.cs | 15 +++ .../NoopImplementations/NoopMailService.cs | 1 + .../RequestSMAccessControllerTests.cs | 86 +++++++++++++++++ 13 files changed, 366 insertions(+) create mode 100644 bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Requests/RequestSMAccessCommand.cs create mode 100644 bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/Requests/RequestSMAccessCommandTests.cs create mode 100644 src/Api/SecretsManager/Controllers/RequestSMAccessController.cs create mode 100644 src/Api/SecretsManager/Models/Request/RequestSMAccessRequestModel.cs create mode 100644 src/Core/MailTemplates/Handlebars/SecretsManagerAccessRequest.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/SecretsManagerAccessRequest.text.hbs create mode 100644 src/Core/SecretsManager/Commands/Requests/Interfaces/IRequestSMAccessCommand.cs create mode 100644 src/Core/SecretsManager/Models/Mail/RequestSecretsManagerAccessViewModel.cs create mode 100644 test/Api.Test/SecretsManager/Controllers/RequestSMAccessControllerTests.cs diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Requests/RequestSMAccessCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Requests/RequestSMAccessCommand.cs new file mode 100644 index 0000000000..440c0dfeec --- /dev/null +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Requests/RequestSMAccessCommand.cs @@ -0,0 +1,34 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.SecretsManager.Commands.Requests.Interfaces; +using Bit.Core.Services; + +namespace Bit.Commercial.Core.SecretsManager.Commands.Requests; + +public class RequestSMAccessCommand : IRequestSMAccessCommand +{ + private readonly IMailService _mailService; + + public RequestSMAccessCommand( + IMailService mailService) + { + _mailService = mailService; + } + + public async Task SendRequestAccessToSM(Organization organization, ICollection orgUsers, User user, string emailContent) + { + var emailList = orgUsers.Where(o => o.Type <= OrganizationUserType.Admin) + .Select(a => a.Email).Distinct().ToList(); + + if (!emailList.Any()) + { + throw new BadRequestException("The organization is in an invalid state. Please contact Customer Support."); + } + + var userRequestingAccess = user.Name ?? user.Email; + await _mailService.SendRequestSMAccessToAdminEmailAsync(emailList, organization.Name, userRequestingAccess, emailContent); + } +} diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs index 24051eec79..8d20100281 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs @@ -6,6 +6,7 @@ using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies; using Bit.Commercial.Core.SecretsManager.Commands.AccessTokens; using Bit.Commercial.Core.SecretsManager.Commands.Porting; using Bit.Commercial.Core.SecretsManager.Commands.Projects; +using Bit.Commercial.Core.SecretsManager.Commands.Requests; using Bit.Commercial.Core.SecretsManager.Commands.Secrets; using Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts; using Bit.Commercial.Core.SecretsManager.Commands.Trash; @@ -18,6 +19,7 @@ using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces; using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces; using Bit.Core.SecretsManager.Commands.Porting.Interfaces; using Bit.Core.SecretsManager.Commands.Projects.Interfaces; +using Bit.Core.SecretsManager.Commands.Requests.Interfaces; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces; using Bit.Core.SecretsManager.Commands.Trash.Interfaces; @@ -56,6 +58,7 @@ public static class SecretsManagerCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/Requests/RequestSMAccessCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/Requests/RequestSMAccessCommandTests.cs new file mode 100644 index 0000000000..e9387deecf --- /dev/null +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/Requests/RequestSMAccessCommandTests.cs @@ -0,0 +1,96 @@ +using Bit.Commercial.Core.SecretsManager.Commands.Requests; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Core.Test.SecretsManager.Commands.Requests; + +[SutProviderCustomize] +public class RequestSMAccessCommandTests +{ + [Theory] + [BitAutoData] + public async Task SendRequestAccessToSM_Success( + User user, + Organization organization, + ICollection orgUsers, + string emailContent, + SutProvider sutProvider) + { + foreach (var userDetails in orgUsers) + { + userDetails.Type = OrganizationUserType.Admin; + } + + orgUsers.First().Type = OrganizationUserType.Owner; + + await sutProvider.Sut.SendRequestAccessToSM(organization, orgUsers, user, emailContent); + + var adminEmailList = orgUsers + .Where(o => o.Type <= OrganizationUserType.Admin) + .Select(a => a.Email) + .Distinct() + .ToList(); + + await sutProvider.GetDependency() + .Received(1) + .SendRequestSMAccessToAdminEmailAsync(Arg.Is(AssertHelper.AssertPropertyEqual(adminEmailList)), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task SendRequestAccessToSM_NoAdmins_ThrowsBadRequestException( + User user, + Organization organization, + ICollection orgUsers, + string emailContent, + SutProvider sutProvider) + { + // Set OrgUsers so they are only users, no admins or owners + foreach (OrganizationUserUserDetails userDetails in orgUsers) + { + userDetails.Type = OrganizationUserType.User; + } + + await Assert.ThrowsAsync(() => sutProvider.Sut.SendRequestAccessToSM(organization, orgUsers, user, emailContent)); + } + + + [Theory] + [BitAutoData] + public async Task SendRequestAccessToSM_SomeAdmins_EmailListIsAsExpected( + User user, + Organization organization, + ICollection orgUsers, + string emailContent, + SutProvider sutProvider) + { + foreach (OrganizationUserUserDetails userDetails in orgUsers) + { + userDetails.Type = OrganizationUserType.User; + } + + // Make the first orgUser an admin so it's a mix of Admin + Users + orgUsers.First().Type = OrganizationUserType.Admin; + + var adminEmailList = orgUsers + .Where(o => o.Type == OrganizationUserType.Admin) // Filter by Admin type + .Select(a => a.Email) + .Distinct() + .ToList(); + + await sutProvider.Sut.SendRequestAccessToSM(organization, orgUsers, user, emailContent); + + await sutProvider.GetDependency() + .Received(1) + .SendRequestSMAccessToAdminEmailAsync(Arg.Is(AssertHelper.AssertPropertyEqual(adminEmailList)), Arg.Any(), Arg.Any(), Arg.Any()); + } +} diff --git a/src/Api/SecretsManager/Controllers/RequestSMAccessController.cs b/src/Api/SecretsManager/Controllers/RequestSMAccessController.cs new file mode 100644 index 0000000000..c9b393bb2c --- /dev/null +++ b/src/Api/SecretsManager/Controllers/RequestSMAccessController.cs @@ -0,0 +1,55 @@ +using Bit.Api.SecretsManager.Models.Request; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.SecretsManager.Commands.Requests.Interfaces; +using Bit.Core.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.SecretsManager.Controllers; + +[Route("request-access")] +[Authorize("Web")] +public class RequestSMAccessController : Controller +{ + private readonly IRequestSMAccessCommand _requestSMAccessCommand; + private readonly IUserService _userService; + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly ICurrentContext _currentContext; + + public RequestSMAccessController( + IRequestSMAccessCommand requestSMAccessCommand, IUserService userService, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, ICurrentContext currentContext) + { + _requestSMAccessCommand = requestSMAccessCommand; + _userService = userService; + _organizationRepository = organizationRepository; + _organizationUserRepository = organizationUserRepository; + _currentContext = currentContext; + } + + [HttpPost("request-sm-access")] + public async Task RequestSMAccessFromAdmins([FromBody] RequestSMAccessRequestModel model) + { + var user = await _userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + if (!await _currentContext.OrganizationUser(model.OrganizationId)) + { + throw new NotFoundException(); + } + + var organization = await _organizationRepository.GetByIdAsync(model.OrganizationId); + if (organization == null) + { + throw new NotFoundException(); + } + + var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organization.Id); + await _requestSMAccessCommand.SendRequestAccessToSM(organization, orgUsers, user, model.EmailContent); + } +} diff --git a/src/Api/SecretsManager/Models/Request/RequestSMAccessRequestModel.cs b/src/Api/SecretsManager/Models/Request/RequestSMAccessRequestModel.cs new file mode 100644 index 0000000000..1f05bad933 --- /dev/null +++ b/src/Api/SecretsManager/Models/Request/RequestSMAccessRequestModel.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Api.SecretsManager.Models.Request; + +public class RequestSMAccessRequestModel +{ + [Required] + public Guid OrganizationId { get; set; } + [Required(ErrorMessage = "Add a note is a required field")] + public string EmailContent { get; set; } +} diff --git a/src/Core/MailTemplates/Handlebars/SecretsManagerAccessRequest.html.hbs b/src/Core/MailTemplates/Handlebars/SecretsManagerAccessRequest.html.hbs new file mode 100644 index 0000000000..501e09cf1d --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/SecretsManagerAccessRequest.html.hbs @@ -0,0 +1,27 @@ +{{#>FullHtmlLayout}} + + + + +
+ + + + + + + +
+ {{UserNameRequestingAccess}} has requested access to secrets manager for {{OrgName}}:

+
{{EmailContent}} - {{UserNameRequestingAccess}}
+
+ + Contact Bitwarden + +
+
+
Stay safe and secure,
+ The Bitwarden Team +
+ +{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/SecretsManagerAccessRequest.text.hbs b/src/Core/MailTemplates/Handlebars/SecretsManagerAccessRequest.text.hbs new file mode 100644 index 0000000000..62e9b74917 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/SecretsManagerAccessRequest.text.hbs @@ -0,0 +1,17 @@ +{{#>FullTextLayout}} + +{{UserNameRequestingAccess}} has requested access to secrets manager for {{OrgName}}: + +============ + +{{EmailContent}} - {{UserNameRequestingAccess}} + +============ + +Contact Bitwarden (https://bitwarden.com/contact-sales/?utm_source=sm_request_access_email&utm_medium=email) + +============ + +Stay safe and secure, +The Bitwarden Team +{{/FullTextLayout}} diff --git a/src/Core/SecretsManager/Commands/Requests/Interfaces/IRequestSMAccessCommand.cs b/src/Core/SecretsManager/Commands/Requests/Interfaces/IRequestSMAccessCommand.cs new file mode 100644 index 0000000000..330c385530 --- /dev/null +++ b/src/Core/SecretsManager/Commands/Requests/Interfaces/IRequestSMAccessCommand.cs @@ -0,0 +1,10 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Entities; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; + +namespace Bit.Core.SecretsManager.Commands.Requests.Interfaces; + +public interface IRequestSMAccessCommand +{ + Task SendRequestAccessToSM(Organization organization, ICollection orgUsers, User user, string emailContent); +} diff --git a/src/Core/SecretsManager/Models/Mail/RequestSecretsManagerAccessViewModel.cs b/src/Core/SecretsManager/Models/Mail/RequestSecretsManagerAccessViewModel.cs new file mode 100644 index 0000000000..1e35f97d1d --- /dev/null +++ b/src/Core/SecretsManager/Models/Mail/RequestSecretsManagerAccessViewModel.cs @@ -0,0 +1,10 @@ +using Bit.Core.Models.Mail; + +namespace Bit.Core.SecretsManager.Models.Mail; + +public class RequestSecretsManagerAccessViewModel : BaseMailModel +{ + public string UserNameRequestingAccess { get; set; } + public string OrgName { get; set; } + public string EmailContent { get; set; } +} diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 14a08e9103..a9f8dfb3f4 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -81,5 +81,6 @@ public interface IMailService Task SendTrialInitiationEmailAsync(string email); Task SendInitiateDeletProviderEmailAsync(string email, Provider provider, string token); Task SendInitiateDeleteOrganzationEmailAsync(string email, Organization organization, string token); + Task SendRequestSMAccessToAdminEmailAsync(IEnumerable adminEmails, string organizationName, string userRequestingAccess, string emailContent); } diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index d4f56e4723..9d5580715e 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -8,6 +8,7 @@ using Bit.Core.Entities; using Bit.Core.Models.Mail; using Bit.Core.Models.Mail.FamiliesForEnterprise; using Bit.Core.Models.Mail.Provider; +using Bit.Core.SecretsManager.Models.Mail; using Bit.Core.Settings; using Bit.Core.Utilities; using HandlebarsDotNet; @@ -395,6 +396,20 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendRequestSMAccessToAdminEmailAsync(IEnumerable emails, string organizationName, string requestingUserName, string emailContent) + { + var message = CreateDefaultMessage("Access Requested for Secrets Manager", emails); + var model = new RequestSecretsManagerAccessViewModel + { + OrgName = CoreHelpers.SanitizeForEmail(organizationName, false), + UserNameRequestingAccess = CoreHelpers.SanitizeForEmail(requestingUserName, false), + EmailContent = CoreHelpers.SanitizeForEmail(emailContent, false), + }; + await AddMessageContentAsync(message, "SecretsManagerAccessRequest", model); + message.Category = "SecretsManagerAccessRequest"; + await _mailDeliveryService.SendEmailAsync(message); + } + public async Task SendNewDeviceLoggedInEmail(string email, string deviceType, DateTime timestamp, string ip) { var message = CreateDefaultMessage($"New Device Logged In From {deviceType}", email); diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index 998714d13b..27e920cbe8 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -280,5 +280,6 @@ public class NoopMailService : IMailService { return Task.FromResult(0); } + public Task SendRequestSMAccessToAdminEmailAsync(IEnumerable adminEmails, string organizationName, string userRequestingAccess, string emailContent) => throw new NotImplementedException(); } diff --git a/test/Api.Test/SecretsManager/Controllers/RequestSMAccessControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/RequestSMAccessControllerTests.cs new file mode 100644 index 0000000000..3c76246a0c --- /dev/null +++ b/test/Api.Test/SecretsManager/Controllers/RequestSMAccessControllerTests.cs @@ -0,0 +1,86 @@ +using System.Security.Claims; +using Bit.Api.SecretsManager.Controllers; +using Bit.Api.SecretsManager.Models.Request; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; +using Bit.Core.SecretsManager.Commands.Requests.Interfaces; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Xunit; + +namespace Bit.Api.Test.SecretsManager.Controllers; + +[ControllerCustomize(typeof(RequestSMAccessController))] +[SutProviderCustomize] +public class RequestSMAccessControllerTests +{ + [Theory] + [BitAutoData] + public async Task RequestSMAccessFromAdmins_WhenSendingNoModel_ShouldThrowNotFoundException( + User user, SutProvider sutProvider) + { + sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).Returns(user); + sutProvider.GetDependency().GetByIdentifierAsync(Arg.Any()).ReturnsNullForAnyArgs(); + + await Assert.ThrowsAsync(() => sutProvider.Sut.RequestSMAccessFromAdmins(new RequestSMAccessRequestModel())); + } + + [Theory] + [BitAutoData] + public async Task RequestSMAccessFromAdmins_WhenSendingValidData_ShouldSucceed( + User user, + RequestSMAccessRequestModel model, + Core.AdminConsole.Entities.Organization org, + ICollection orgUsers, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(model.OrganizationId).Returns(org); + sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).Returns(user); + sutProvider.GetDependency().GetManyDetailsByOrganizationAsync(org.Id).Returns(orgUsers); + sutProvider.GetDependency().OrganizationUser(model.OrganizationId).Returns(true); + + await sutProvider.Sut.RequestSMAccessFromAdmins(model); + + //Also check that the command was called + await sutProvider.GetDependency() + .Received(1) + .SendRequestAccessToSM(org, orgUsers, user, model.EmailContent); + } + + [Theory] + [BitAutoData] + public async Task RequestSMAccessFromAdmins_WhenUserInvalid_ShouldThrowBadRequestException(RequestSMAccessRequestModel model, SutProvider sutProvider) + { + sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).ReturnsNullForAnyArgs(); + + await Assert.ThrowsAsync(() => sutProvider.Sut.RequestSMAccessFromAdmins(model)); + } + + [Theory] + [BitAutoData] + public async Task RequestSMAccessFromAdmins_WhenOrgInvalid_ShouldThrowNotFoundException(RequestSMAccessRequestModel model, User user, SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdentifierAsync(Arg.Any()).ReturnsNullForAnyArgs(); + sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).ReturnsForAnyArgs(user); + sutProvider.GetDependency().OrganizationUser(model.OrganizationId).Returns(true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.RequestSMAccessFromAdmins(model)); + } + + [Theory] + [BitAutoData] + public async Task RequestSMAccessFromAdmins_WhenOrgUserInvalid_ShouldThrowNotFoundException(RequestSMAccessRequestModel model, User user, SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdentifierAsync(Arg.Any()).ReturnsNullForAnyArgs(); + sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).ReturnsForAnyArgs(user); + sutProvider.GetDependency().OrganizationUser(model.OrganizationId).Returns(false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.RequestSMAccessFromAdmins(model)); + } +}