mirror of
https://github.com/bitwarden/server.git
synced 2024-11-28 13:15:12 +01:00
[AC-292] Public Api - allow configuration of custom permissions (#4022)
* Also refactor OrganizationService user invite methods
This commit is contained in:
parent
0189952e1f
commit
357ac4f40a
@ -554,7 +554,7 @@ public class ProviderService : IProviderService
|
|||||||
]
|
]
|
||||||
: Array.Empty<CollectionAccessSelection>();
|
: Array.Empty<CollectionAccessSelection>();
|
||||||
|
|
||||||
await _organizationService.InviteUsersAsync(organization.Id, user.Id,
|
await _organizationService.InviteUsersAsync(organization.Id, user.Id, systemUser: null,
|
||||||
new (OrganizationUserInvite, string)[]
|
new (OrganizationUserInvite, string)[]
|
||||||
{
|
{
|
||||||
(
|
(
|
||||||
|
@ -1,8 +1,67 @@
|
|||||||
namespace Bit.Scim.Models;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
public class ScimUserRequestModel : BaseScimUserModel
|
public class ScimUserRequestModel : BaseScimUserModel
|
||||||
{
|
{
|
||||||
public ScimUserRequestModel()
|
public ScimUserRequestModel()
|
||||||
: base(false)
|
: base(false)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
|
public OrganizationUserInvite ToOrganizationUserInvite(ScimProviderType scimProvider)
|
||||||
|
{
|
||||||
|
return new OrganizationUserInvite
|
||||||
|
{
|
||||||
|
Emails = new[] { EmailForInvite(scimProvider) },
|
||||||
|
|
||||||
|
// Permissions cannot be set via SCIM so we use default values
|
||||||
|
Type = OrganizationUserType.User,
|
||||||
|
AccessAll = false,
|
||||||
|
Collections = new List<CollectionAccessSelection>(),
|
||||||
|
Groups = new List<Guid>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string EmailForInvite(ScimProviderType scimProvider)
|
||||||
|
{
|
||||||
|
var email = PrimaryEmail?.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(email))
|
||||||
|
{
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (scimProvider)
|
||||||
|
{
|
||||||
|
case ScimProviderType.AzureAd:
|
||||||
|
return UserName?.ToLowerInvariant();
|
||||||
|
default:
|
||||||
|
email = WorkEmail?.ToLowerInvariant();
|
||||||
|
if (string.IsNullOrWhiteSpace(email))
|
||||||
|
{
|
||||||
|
email = Emails?.FirstOrDefault()?.Value?.ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ExternalIdForInvite()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(ExternalId))
|
||||||
|
{
|
||||||
|
return ExternalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(UserName))
|
||||||
|
{
|
||||||
|
return UserName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CoreHelpers.RandomString(15);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Data;
|
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Bit.Scim.Context;
|
using Bit.Scim.Context;
|
||||||
using Bit.Scim.Models;
|
using Bit.Scim.Models;
|
||||||
using Bit.Scim.Users.Interfaces;
|
using Bit.Scim.Users.Interfaces;
|
||||||
@ -36,23 +33,11 @@ public class PostUserCommand : IPostUserCommand
|
|||||||
|
|
||||||
public async Task<OrganizationUserUserDetails> PostUserAsync(Guid organizationId, ScimUserRequestModel model)
|
public async Task<OrganizationUserUserDetails> PostUserAsync(Guid organizationId, ScimUserRequestModel model)
|
||||||
{
|
{
|
||||||
var email = model.PrimaryEmail?.ToLowerInvariant();
|
var scimProvider = _scimContext.RequestScimProvider;
|
||||||
if (string.IsNullOrWhiteSpace(email))
|
var invite = model.ToOrganizationUserInvite(scimProvider);
|
||||||
{
|
|
||||||
switch (_scimContext.RequestScimProvider)
|
var email = invite.Emails.Single();
|
||||||
{
|
var externalId = model.ExternalIdForInvite();
|
||||||
case ScimProviderType.AzureAd:
|
|
||||||
email = model.UserName?.ToLowerInvariant();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
email = model.WorkEmail?.ToLowerInvariant();
|
|
||||||
if (string.IsNullOrWhiteSpace(email))
|
|
||||||
{
|
|
||||||
email = model.Emails?.FirstOrDefault()?.Value?.ToLowerInvariant();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(email) || !model.Active)
|
if (string.IsNullOrWhiteSpace(email) || !model.Active)
|
||||||
{
|
{
|
||||||
@ -66,20 +51,6 @@ public class PostUserCommand : IPostUserCommand
|
|||||||
throw new ConflictException();
|
throw new ConflictException();
|
||||||
}
|
}
|
||||||
|
|
||||||
string externalId = null;
|
|
||||||
if (!string.IsNullOrWhiteSpace(model.ExternalId))
|
|
||||||
{
|
|
||||||
externalId = model.ExternalId;
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrWhiteSpace(model.UserName))
|
|
||||||
{
|
|
||||||
externalId = model.UserName;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
externalId = CoreHelpers.RandomString(15);
|
|
||||||
}
|
|
||||||
|
|
||||||
var orgUserByExternalId = orgUsers.FirstOrDefault(ou => ou.ExternalId == externalId);
|
var orgUserByExternalId = orgUsers.FirstOrDefault(ou => ou.ExternalId == externalId);
|
||||||
if (orgUserByExternalId != null)
|
if (orgUserByExternalId != null)
|
||||||
{
|
{
|
||||||
@ -87,12 +58,11 @@ public class PostUserCommand : IPostUserCommand
|
|||||||
}
|
}
|
||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
|
||||||
var hasStandaloneSecretsManager = await _paymentService.HasSecretsManagerStandalone(organization);
|
var hasStandaloneSecretsManager = await _paymentService.HasSecretsManagerStandalone(organization);
|
||||||
|
invite.AccessSecretsManager = hasStandaloneSecretsManager;
|
||||||
|
|
||||||
var invitedOrgUser = await _organizationService.InviteUserAsync(organizationId, EventSystemUser.SCIM, email,
|
var invitedOrgUser = await _organizationService.InviteUserAsync(organizationId, invitingUserId: null, EventSystemUser.SCIM,
|
||||||
OrganizationUserType.User, false, externalId, new List<CollectionAccessSelection>(), new List<Guid>(), hasStandaloneSecretsManager);
|
invite, externalId);
|
||||||
|
|
||||||
var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(invitedOrgUser.Id);
|
var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(invitedOrgUser.Id);
|
||||||
|
|
||||||
return orgUser;
|
return orgUser;
|
||||||
|
@ -631,7 +631,7 @@ public class ProviderServiceTests
|
|||||||
.Received().LogProviderOrganizationEventAsync(providerOrganization,
|
.Received().LogProviderOrganizationEventAsync(providerOrganization,
|
||||||
EventType.ProviderOrganization_Created);
|
EventType.ProviderOrganization_Created);
|
||||||
await sutProvider.GetDependency<IOrganizationService>()
|
await sutProvider.GetDependency<IOrganizationService>()
|
||||||
.Received().InviteUsersAsync(organization.Id, user.Id, Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(
|
.Received().InviteUsersAsync(organization.Id, user.Id, systemUser: null, Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(
|
||||||
t => t.Count() == 1 &&
|
t => t.Count() == 1 &&
|
||||||
t.First().Item1.Emails.Count() == 1 &&
|
t.First().Item1.Emails.Count() == 1 &&
|
||||||
t.First().Item1.Emails.First() == clientOwnerEmail &&
|
t.First().Item1.Emails.First() == clientOwnerEmail &&
|
||||||
@ -709,6 +709,7 @@ public class ProviderServiceTests
|
|||||||
.InviteUsersAsync(
|
.InviteUsersAsync(
|
||||||
organization.Id,
|
organization.Id,
|
||||||
user.Id,
|
user.Id,
|
||||||
|
systemUser: null,
|
||||||
Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(
|
Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(
|
||||||
t =>
|
t =>
|
||||||
t.Count() == 1 &&
|
t.Count() == 1 &&
|
||||||
@ -740,7 +741,7 @@ public class ProviderServiceTests
|
|||||||
.Received().LogProviderOrganizationEventAsync(providerOrganization,
|
.Received().LogProviderOrganizationEventAsync(providerOrganization,
|
||||||
EventType.ProviderOrganization_Created);
|
EventType.ProviderOrganization_Created);
|
||||||
await sutProvider.GetDependency<IOrganizationService>()
|
await sutProvider.GetDependency<IOrganizationService>()
|
||||||
.Received().InviteUsersAsync(organization.Id, user.Id, Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(
|
.Received().InviteUsersAsync(organization.Id, user.Id, systemUser: null, Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(
|
||||||
t => t.Count() == 1 &&
|
t => t.Count() == 1 &&
|
||||||
t.First().Item1.Emails.Count() == 1 &&
|
t.First().Item1.Emails.Count() == 1 &&
|
||||||
t.First().Item1.Emails.First() == clientOwnerEmail &&
|
t.First().Item1.Emails.First() == clientOwnerEmail &&
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.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.Models.Business;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -39,15 +39,27 @@ public class PostUserCommandTests
|
|||||||
sutProvider.GetDependency<IPaymentService>().HasSecretsManagerStandalone(organization).Returns(true);
|
sutProvider.GetDependency<IPaymentService>().HasSecretsManagerStandalone(organization).Returns(true);
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationService>()
|
sutProvider.GetDependency<IOrganizationService>()
|
||||||
.InviteUserAsync(organizationId, EventSystemUser.SCIM, scimUserRequestModel.PrimaryEmail.ToLowerInvariant(),
|
.InviteUserAsync(organizationId, invitingUserId: null, EventSystemUser.SCIM,
|
||||||
OrganizationUserType.User, false, externalId, Arg.Any<List<CollectionAccessSelection>>(),
|
Arg.Is<OrganizationUserInvite>(i =>
|
||||||
Arg.Any<List<Guid>>(), true)
|
i.Emails.Single().Equals(scimUserRequestModel.PrimaryEmail.ToLowerInvariant()) &&
|
||||||
|
i.Type == OrganizationUserType.User &&
|
||||||
|
!i.AccessAll &&
|
||||||
|
!i.Collections.Any() &&
|
||||||
|
!i.Groups.Any() &&
|
||||||
|
i.AccessSecretsManager), externalId)
|
||||||
.Returns(newUser);
|
.Returns(newUser);
|
||||||
|
|
||||||
var user = await sutProvider.Sut.PostUserAsync(organizationId, scimUserRequestModel);
|
var user = await sutProvider.Sut.PostUserAsync(organizationId, scimUserRequestModel);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).InviteUserAsync(organizationId, EventSystemUser.SCIM, scimUserRequestModel.PrimaryEmail.ToLowerInvariant(),
|
await sutProvider.GetDependency<IOrganizationService>().Received(1).InviteUserAsync(organizationId,
|
||||||
OrganizationUserType.User, false, scimUserRequestModel.ExternalId, Arg.Any<List<CollectionAccessSelection>>(), Arg.Any<List<Guid>>(), true);
|
invitingUserId: null, EventSystemUser.SCIM,
|
||||||
|
Arg.Is<OrganizationUserInvite>(i =>
|
||||||
|
i.Emails.Single().Equals(scimUserRequestModel.PrimaryEmail.ToLowerInvariant()) &&
|
||||||
|
i.Type == OrganizationUserType.User &&
|
||||||
|
!i.AccessAll &&
|
||||||
|
!i.Collections.Any() &&
|
||||||
|
!i.Groups.Any() &&
|
||||||
|
i.AccessSecretsManager), externalId);
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetDetailsByIdAsync(newUser.Id);
|
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetDetailsByIdAsync(newUser.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,7 +225,7 @@ public class OrganizationUsersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var userId = _userService.GetProperUserId(User);
|
var userId = _userService.GetProperUserId(User);
|
||||||
await _organizationService.InviteUsersAsync(orgId, userId.Value,
|
await _organizationService.InviteUsersAsync(orgId, userId.Value, systemUser: null,
|
||||||
new (OrganizationUserInvite, string)[] { (new OrganizationUserInvite(model.ToData()), null) });
|
new (OrganizationUserInvite, string)[] { (new OrganizationUserInvite(model.ToData()), null) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,10 +127,11 @@ public class MembersController : Controller
|
|||||||
public async Task<IActionResult> Post([FromBody] MemberCreateRequestModel model)
|
public async Task<IActionResult> Post([FromBody] MemberCreateRequestModel model)
|
||||||
{
|
{
|
||||||
var flexibleCollectionsIsEnabled = await FlexibleCollectionsIsEnabledAsync(_currentContext.OrganizationId.Value);
|
var flexibleCollectionsIsEnabled = await FlexibleCollectionsIsEnabledAsync(_currentContext.OrganizationId.Value);
|
||||||
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection(flexibleCollectionsIsEnabled)).ToList();
|
var invite = model.ToOrganizationUserInvite(flexibleCollectionsIsEnabled);
|
||||||
|
|
||||||
var user = await _organizationService.InviteUserAsync(_currentContext.OrganizationId.Value, null,
|
var user = await _organizationService.InviteUserAsync(_currentContext.OrganizationId.Value, null,
|
||||||
model.Email, model.Type.Value, model.AccessAll.Value, model.ExternalId, associations, model.Groups);
|
systemUser: null, invite, model.ExternalId);
|
||||||
var response = new MemberResponseModel(user, associations, flexibleCollectionsIsEnabled);
|
var response = new MemberResponseModel(user, invite.Collections, flexibleCollectionsIsEnabled);
|
||||||
return new JsonResult(response);
|
return new JsonResult(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
#nullable enable
|
||||||
|
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
@ -21,6 +23,11 @@ public abstract class MemberBaseModel
|
|||||||
AccessAll = user.AccessAll;
|
AccessAll = user.AccessAll;
|
||||||
ExternalId = user.ExternalId;
|
ExternalId = user.ExternalId;
|
||||||
ResetPasswordEnrolled = user.ResetPasswordKey != null;
|
ResetPasswordEnrolled = user.ResetPasswordKey != null;
|
||||||
|
|
||||||
|
if (Type == OrganizationUserType.Custom)
|
||||||
|
{
|
||||||
|
Permissions = new PermissionsModel(user.GetPermissions());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public MemberBaseModel(OrganizationUserUserDetails user, bool flexibleCollectionsEnabled)
|
public MemberBaseModel(OrganizationUserUserDetails user, bool flexibleCollectionsEnabled)
|
||||||
@ -34,6 +41,11 @@ public abstract class MemberBaseModel
|
|||||||
AccessAll = user.AccessAll;
|
AccessAll = user.AccessAll;
|
||||||
ExternalId = user.ExternalId;
|
ExternalId = user.ExternalId;
|
||||||
ResetPasswordEnrolled = user.ResetPasswordKey != null;
|
ResetPasswordEnrolled = user.ResetPasswordKey != null;
|
||||||
|
|
||||||
|
if (Type == OrganizationUserType.Custom)
|
||||||
|
{
|
||||||
|
Permissions = new PermissionsModel(user.GetPermissions());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -59,6 +71,11 @@ public abstract class MemberBaseModel
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
public bool ResetPasswordEnrolled { get; set; }
|
public bool ResetPasswordEnrolled { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The member's custom permissions if the member has a Custom role. If not supplied, all custom permissions will
|
||||||
|
/// default to false.
|
||||||
|
/// </summary>
|
||||||
|
public PermissionsModel? Permissions { get; set; }
|
||||||
|
|
||||||
// TODO: AC-2188 - Remove this method when the custom users with no other permissions than 'Edit/Delete Assigned Collections' are migrated
|
// TODO: AC-2188 - Remove this method when the custom users with no other permissions than 'Edit/Delete Assigned Collections' are migrated
|
||||||
private OrganizationUserType GetFlexibleCollectionsUserType(OrganizationUserType type, Permissions permissions)
|
private OrganizationUserType GetFlexibleCollectionsUserType(OrganizationUserType type, Permissions permissions)
|
||||||
|
67
src/Api/AdminConsole/Public/Models/PermissionsModel.cs
Normal file
67
src/Api/AdminConsole/Public/Models/PermissionsModel.cs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
|
||||||
|
namespace Bit.Api.AdminConsole.Public.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a member's custom permissions if the member has a Custom role.
|
||||||
|
/// </summary>
|
||||||
|
public class PermissionsModel
|
||||||
|
{
|
||||||
|
[JsonConstructor]
|
||||||
|
public PermissionsModel() { }
|
||||||
|
public PermissionsModel(Permissions? data)
|
||||||
|
{
|
||||||
|
if (data is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessEventLogs = data.AccessEventLogs;
|
||||||
|
AccessImportExport = data.AccessImportExport;
|
||||||
|
AccessReports = data.AccessReports;
|
||||||
|
CreateNewCollections = data.CreateNewCollections;
|
||||||
|
EditAnyCollection = data.EditAnyCollection;
|
||||||
|
DeleteAnyCollection = data.DeleteAnyCollection;
|
||||||
|
ManageGroups = data.ManageGroups;
|
||||||
|
ManagePolicies = data.ManagePolicies;
|
||||||
|
ManageSso = data.ManageSso;
|
||||||
|
ManageUsers = data.ManageUsers;
|
||||||
|
ManageResetPassword = data.ManageResetPassword;
|
||||||
|
ManageScim = data.ManageScim;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AccessEventLogs { get; set; }
|
||||||
|
public bool AccessImportExport { get; set; }
|
||||||
|
public bool AccessReports { get; set; }
|
||||||
|
public bool CreateNewCollections { get; set; }
|
||||||
|
public bool EditAnyCollection { get; set; }
|
||||||
|
public bool DeleteAnyCollection { get; set; }
|
||||||
|
public bool ManageGroups { get; set; }
|
||||||
|
public bool ManagePolicies { get; set; }
|
||||||
|
public bool ManageSso { get; set; }
|
||||||
|
public bool ManageUsers { get; set; }
|
||||||
|
public bool ManageResetPassword { get; set; }
|
||||||
|
public bool ManageScim { get; set; }
|
||||||
|
|
||||||
|
public Permissions ToData()
|
||||||
|
{
|
||||||
|
return new Permissions
|
||||||
|
{
|
||||||
|
AccessEventLogs = AccessEventLogs,
|
||||||
|
AccessImportExport = AccessImportExport,
|
||||||
|
AccessReports = AccessReports,
|
||||||
|
CreateNewCollections = CreateNewCollections,
|
||||||
|
EditAnyCollection = EditAnyCollection,
|
||||||
|
DeleteAnyCollection = DeleteAnyCollection,
|
||||||
|
ManageGroups = ManageGroups,
|
||||||
|
ManagePolicies = ManagePolicies,
|
||||||
|
ManageSso = ManageSso,
|
||||||
|
ManageUsers = ManageUsers,
|
||||||
|
ManageResetPassword = ManageResetPassword,
|
||||||
|
ManageScim = ManageScim
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Api.AdminConsole.Public.Models.Request;
|
namespace Bit.Api.AdminConsole.Public.Models.Request;
|
||||||
@ -19,4 +21,24 @@ public class MemberCreateRequestModel : MemberUpdateRequestModel
|
|||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OrganizationUserInvite ToOrganizationUserInvite(bool flexibleCollectionsIsEnabled)
|
||||||
|
{
|
||||||
|
var invite = new OrganizationUserInvite
|
||||||
|
{
|
||||||
|
Emails = new[] { Email },
|
||||||
|
Type = Type.Value,
|
||||||
|
AccessAll = AccessAll.Value,
|
||||||
|
Collections = Collections?.Select(c => c.ToCollectionAccessSelection(flexibleCollectionsIsEnabled)).ToList(),
|
||||||
|
Groups = Groups
|
||||||
|
};
|
||||||
|
|
||||||
|
// Permissions property is optional for backwards compatibility with existing usage
|
||||||
|
if (Type is OrganizationUserType.Custom && Permissions is not null)
|
||||||
|
{
|
||||||
|
invite.Permissions = Permissions.ToData();
|
||||||
|
}
|
||||||
|
|
||||||
|
return invite;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
using Bit.Core.Entities;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
namespace Bit.Api.AdminConsole.Public.Models.Request;
|
namespace Bit.Api.AdminConsole.Public.Models.Request;
|
||||||
|
|
||||||
public class MemberUpdateRequestModel : MemberBaseModel
|
public class MemberUpdateRequestModel : MemberBaseModel, IValidatableObject
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The associated collections that this member can access.
|
/// The associated collections that this member can access.
|
||||||
@ -19,6 +21,21 @@ public class MemberUpdateRequestModel : MemberBaseModel
|
|||||||
existingUser.Type = Type.Value;
|
existingUser.Type = Type.Value;
|
||||||
existingUser.AccessAll = AccessAll.Value;
|
existingUser.AccessAll = AccessAll.Value;
|
||||||
existingUser.ExternalId = ExternalId;
|
existingUser.ExternalId = ExternalId;
|
||||||
|
|
||||||
|
// Permissions property is optional for backwards compatibility with existing usage
|
||||||
|
if (existingUser.Type is OrganizationUserType.Custom && Permissions is not null)
|
||||||
|
{
|
||||||
|
existingUser.SetPermissions(Permissions.ToData());
|
||||||
|
}
|
||||||
|
|
||||||
return existingUser;
|
return existingUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
if (Type is not OrganizationUserType.Custom && Permissions is not null)
|
||||||
|
{
|
||||||
|
yield return new ValidationResult("Only users with the Custom role may use custom permissions.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Bit.Api.Models.Public.Response;
|
using Bit.Api.Models.Public.Response;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -12,6 +13,9 @@ namespace Bit.Api.AdminConsole.Public.Models.Response;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class MemberResponseModel : MemberBaseModel, IResponseModel
|
public class MemberResponseModel : MemberBaseModel, IResponseModel
|
||||||
{
|
{
|
||||||
|
[JsonConstructor]
|
||||||
|
public MemberResponseModel() { }
|
||||||
|
|
||||||
public MemberResponseModel(OrganizationUser user, IEnumerable<CollectionAccessSelection> collections,
|
public MemberResponseModel(OrganizationUser user, IEnumerable<CollectionAccessSelection> collections,
|
||||||
bool flexibleCollectionsEnabled)
|
bool flexibleCollectionsEnabled)
|
||||||
: base(user, flexibleCollectionsEnabled)
|
: base(user, flexibleCollectionsEnabled)
|
||||||
|
@ -35,4 +35,9 @@ public class OrganizationUser : ITableObject<Guid>, IExternal
|
|||||||
return string.IsNullOrWhiteSpace(Permissions) ? null
|
return string.IsNullOrWhiteSpace(Permissions) ? null
|
||||||
: CoreHelpers.LoadClassFromJsonData<Permissions>(Permissions);
|
: CoreHelpers.LoadClassFromJsonData<Permissions>(Permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetPermissions(Permissions permissions)
|
||||||
|
{
|
||||||
|
Permissions = CoreHelpers.ClassToJsonData(permissions);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,14 +43,10 @@ public interface IOrganizationService
|
|||||||
Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated);
|
Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated);
|
||||||
Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type);
|
Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type);
|
||||||
Task DisableTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type);
|
Task DisableTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type);
|
||||||
Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId,
|
Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser,
|
||||||
|
OrganizationUserInvite invite, string externalId);
|
||||||
|
Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser,
|
||||||
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites);
|
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites);
|
||||||
Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, EventSystemUser systemUser,
|
|
||||||
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites);
|
|
||||||
Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
|
|
||||||
OrganizationUserType type, bool accessAll, string externalId, ICollection<CollectionAccessSelection> collections, IEnumerable<Guid> groups);
|
|
||||||
Task<OrganizationUser> InviteUserAsync(Guid organizationId, EventSystemUser systemUser, string email,
|
|
||||||
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups, bool accessSecretsManager);
|
|
||||||
Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
|
Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
|
||||||
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false);
|
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false);
|
||||||
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
||||||
|
@ -957,12 +957,53 @@ public class OrganizationService : IOrganizationService
|
|||||||
await UpdateAsync(organization);
|
await UpdateAsync(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId,
|
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser,
|
||||||
|
OrganizationUserInvite invite, string externalId)
|
||||||
|
{
|
||||||
|
// Ideally OrganizationUserInvite should represent a single user so that this doesn't have to be a runtime check
|
||||||
|
if (invite.Emails.Count() > 1)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("This method can only be used to invite a single user.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Collection associations if org is using latest collection enhancements
|
||||||
|
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
|
||||||
|
if (organizationAbility?.FlexibleCollections ?? false)
|
||||||
|
{
|
||||||
|
var invalidAssociations = invite.Collections?.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords));
|
||||||
|
if (invalidAssociations?.Any() ?? false)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = await InviteUsersAsync(organizationId, invitingUserId, systemUser,
|
||||||
|
new (OrganizationUserInvite, string)[] { (invite, externalId) });
|
||||||
|
|
||||||
|
var result = results.FirstOrDefault();
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("This user has already been invited.");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invite users to an organization.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationId">The organization Id</param>
|
||||||
|
/// <param name="invitingUserId">The current authenticated user who is sending the invite. Only used when inviting via a client app; null if using SCIM or Public API.</param>
|
||||||
|
/// <param name="systemUser">The system user which is sending the invite. Only used when inviting via SCIM; null if using a client app or Public API</param>
|
||||||
|
/// <param name="invites">Details about the users being invited</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser,
|
||||||
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites)
|
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites)
|
||||||
{
|
{
|
||||||
var inviteTypes = new HashSet<OrganizationUserType>(invites.Where(i => i.invite.Type.HasValue)
|
var inviteTypes = new HashSet<OrganizationUserType>(invites.Where(i => i.invite.Type.HasValue)
|
||||||
.Select(i => i.invite.Type.Value));
|
.Select(i => i.invite.Type.Value));
|
||||||
|
|
||||||
|
// If authenticating via a client app, verify the inviting user has permissions
|
||||||
|
// cf. SCIM and Public API have superuser permissions here
|
||||||
if (invitingUserId.HasValue && inviteTypes.Count > 0)
|
if (invitingUserId.HasValue && inviteTypes.Count > 0)
|
||||||
{
|
{
|
||||||
foreach (var (invite, _) in invites)
|
foreach (var (invite, _) in invites)
|
||||||
@ -972,25 +1013,24 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (organizationUsers, events) = await SaveUsersSendInvitesAsync(organizationId, invites, systemUser: null);
|
var (organizationUsers, events) = await SaveUsersSendInvitesAsync(organizationId, invites);
|
||||||
|
|
||||||
await _eventService.LogOrganizationUserEventsAsync(events);
|
if (systemUser.HasValue)
|
||||||
|
|
||||||
return organizationUsers;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, EventSystemUser systemUser,
|
|
||||||
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites)
|
|
||||||
{
|
{
|
||||||
var (organizationUsers, events) = await SaveUsersSendInvitesAsync(organizationId, invites, systemUser);
|
// Log SCIM event
|
||||||
|
await _eventService.LogOrganizationUserEventsAsync(events.Select(e => (e.Item1, e.Item2, systemUser.Value, e.Item3)));
|
||||||
await _eventService.LogOrganizationUserEventsAsync(events.Select(e => (e.Item1, e.Item2, systemUser, e.Item3)));
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Log client app or Public Api event
|
||||||
|
await _eventService.LogOrganizationUserEventsAsync(events);
|
||||||
|
}
|
||||||
|
|
||||||
return organizationUsers;
|
return organizationUsers;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(List<OrganizationUser> organizationUsers, List<(OrganizationUser, EventType, DateTime?)> events)> SaveUsersSendInvitesAsync(Guid organizationId,
|
private async Task<(List<OrganizationUser> organizationUsers, List<(OrganizationUser, EventType, DateTime?)> events)> SaveUsersSendInvitesAsync(Guid organizationId,
|
||||||
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites, EventSystemUser? systemUser)
|
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites)
|
||||||
{
|
{
|
||||||
var organization = await GetOrgById(organizationId);
|
var organization = await GetOrgById(organizationId);
|
||||||
var initialSeatCount = organization.Seats;
|
var initialSeatCount = organization.Seats;
|
||||||
@ -1087,9 +1127,9 @@ public class OrganizationService : IOrganizationService
|
|||||||
RevisionDate = DateTime.UtcNow,
|
RevisionDate = DateTime.UtcNow,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (invite.Permissions != null)
|
if (invite.Type == OrganizationUserType.Custom)
|
||||||
{
|
{
|
||||||
orgUser.Permissions = JsonSerializer.Serialize(invite.Permissions, JsonHelpers.CamelCase);
|
orgUser.SetPermissions(invite.Permissions ?? new Permissions());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!orgUser.AccessAll && invite.Collections.Any())
|
if (!orgUser.AccessAll && invite.Collections.Any())
|
||||||
@ -1667,55 +1707,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
EventType.OrganizationUser_ResetPassword_Enroll : EventType.OrganizationUser_ResetPassword_Withdraw);
|
EventType.OrganizationUser_ResetPassword_Enroll : EventType.OrganizationUser_ResetPassword_Withdraw);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
|
|
||||||
OrganizationUserType type, bool accessAll, string externalId, ICollection<CollectionAccessSelection> collections,
|
|
||||||
IEnumerable<Guid> groups)
|
|
||||||
{
|
|
||||||
// Validate Collection associations if org is using latest collection enhancements
|
|
||||||
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
|
|
||||||
if (organizationAbility?.FlexibleCollections ?? false)
|
|
||||||
{
|
|
||||||
var invalidAssociations = collections?.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords));
|
|
||||||
if (invalidAssociations?.Any() ?? false)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await SaveUserSendInviteAsync(organizationId, invitingUserId, systemUser: null, email, type, accessAll, externalId, collections, groups);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, EventSystemUser systemUser, string email,
|
|
||||||
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections,
|
|
||||||
IEnumerable<Guid> groups, bool accessSecretsManager)
|
|
||||||
{
|
|
||||||
// Collection associations validation not required as they are always an empty list - created via system user (scim)
|
|
||||||
return await SaveUserSendInviteAsync(organizationId, invitingUserId: null, systemUser, email, type, accessAll, externalId, collections, groups, accessSecretsManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<OrganizationUser> SaveUserSendInviteAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser, string email,
|
|
||||||
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups, bool accessSecretsManager = false)
|
|
||||||
{
|
|
||||||
var invite = new OrganizationUserInvite()
|
|
||||||
{
|
|
||||||
Emails = new List<string> { email },
|
|
||||||
Type = type,
|
|
||||||
AccessAll = accessAll,
|
|
||||||
Collections = collections,
|
|
||||||
Groups = groups,
|
|
||||||
AccessSecretsManager = accessSecretsManager
|
|
||||||
};
|
|
||||||
var results = systemUser.HasValue ? await InviteUsersAsync(organizationId, systemUser.Value,
|
|
||||||
new (OrganizationUserInvite, string)[] { (invite, externalId) }) : await InviteUsersAsync(organizationId, invitingUserId,
|
|
||||||
new (OrganizationUserInvite, string)[] { (invite, externalId) });
|
|
||||||
var result = results.FirstOrDefault();
|
|
||||||
if (result == null)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("This user has already been invited.");
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ImportAsync(Guid organizationId,
|
public async Task ImportAsync(Guid organizationId,
|
||||||
Guid? importingUserId,
|
Guid? importingUserId,
|
||||||
IEnumerable<ImportedGroup> groups,
|
IEnumerable<ImportedGroup> groups,
|
||||||
@ -1831,7 +1822,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var invitedUsers = await InviteUsersAsync(organizationId, importingUserId, userInvites);
|
var invitedUsers = await InviteUsersAsync(organizationId, importingUserId, systemUser: null, userInvites);
|
||||||
foreach (var invitedUser in invitedUsers)
|
foreach (var invitedUser in invitedUsers)
|
||||||
{
|
{
|
||||||
existingExternalUsersIdDict.Add(invitedUser.ExternalId, invitedUser.Id);
|
existingExternalUsersIdDict.Add(invitedUser.ExternalId, invitedUser.Id);
|
||||||
|
@ -0,0 +1,254 @@
|
|||||||
|
using System.Net;
|
||||||
|
using Bit.Api.AdminConsole.Public.Models;
|
||||||
|
using Bit.Api.AdminConsole.Public.Models.Request;
|
||||||
|
using Bit.Api.AdminConsole.Public.Models.Response;
|
||||||
|
using Bit.Api.IntegrationTest.Factories;
|
||||||
|
using Bit.Api.IntegrationTest.Helpers;
|
||||||
|
using Bit.Api.Models.Public.Response;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Bit.Test.Common.Helpers;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Api.IntegrationTest.AdminConsole.Public.Controllers;
|
||||||
|
|
||||||
|
public class MembersControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
|
||||||
|
{
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
private readonly ApiApplicationFactory _factory;
|
||||||
|
private readonly LoginHelper _loginHelper;
|
||||||
|
private Organization _organization;
|
||||||
|
private string _ownerEmail;
|
||||||
|
|
||||||
|
public MembersControllerTests(ApiApplicationFactory factory)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
_client = factory.CreateClient();
|
||||||
|
_loginHelper = new LoginHelper(_factory, _client);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
// Create the owner account
|
||||||
|
_ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||||
|
await _factory.LoginWithNewAccount(_ownerEmail);
|
||||||
|
|
||||||
|
// Create the organization
|
||||||
|
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
|
||||||
|
ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
|
||||||
|
|
||||||
|
// Authorize with the organization api key
|
||||||
|
await _loginHelper.LoginWithOrganizationApiKeyAsync(_organization.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DisposeAsync()
|
||||||
|
{
|
||||||
|
_client.Dispose();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task List_Member_Success()
|
||||||
|
{
|
||||||
|
var (userEmail1, orgUser1) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||||
|
OrganizationUserType.Custom, new Permissions { AccessImportExport = true, ManagePolicies = true, AccessReports = true });
|
||||||
|
var (userEmail2, orgUser2) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||||
|
OrganizationUserType.Owner);
|
||||||
|
var (userEmail3, orgUser3) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||||
|
OrganizationUserType.User);
|
||||||
|
var (userEmail4, orgUser4) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||||
|
OrganizationUserType.Admin);
|
||||||
|
|
||||||
|
var response = await _client.GetAsync($"/public/members");
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<ListResponseModel<MemberResponseModel>>();
|
||||||
|
Assert.NotNull(result?.Data);
|
||||||
|
Assert.Equal(5, result.Data.Count());
|
||||||
|
|
||||||
|
// The owner
|
||||||
|
Assert.NotNull(result.Data.SingleOrDefault(m =>
|
||||||
|
m.Email == _ownerEmail && m.Type == OrganizationUserType.Owner));
|
||||||
|
|
||||||
|
// The custom user
|
||||||
|
var user1Result = result.Data.SingleOrDefault(m => m.Email == userEmail1);
|
||||||
|
Assert.Equal(OrganizationUserType.Custom, user1Result.Type);
|
||||||
|
AssertHelper.AssertPropertyEqual(
|
||||||
|
new PermissionsModel { AccessImportExport = true, ManagePolicies = true, AccessReports = true },
|
||||||
|
user1Result.Permissions);
|
||||||
|
|
||||||
|
// Everyone else
|
||||||
|
Assert.NotNull(result.Data.SingleOrDefault(m =>
|
||||||
|
m.Email == userEmail2 && m.Type == OrganizationUserType.Owner));
|
||||||
|
Assert.NotNull(result.Data.SingleOrDefault(m =>
|
||||||
|
m.Email == userEmail3 && m.Type == OrganizationUserType.User));
|
||||||
|
Assert.NotNull(result.Data.SingleOrDefault(m =>
|
||||||
|
m.Email == userEmail4 && m.Type == OrganizationUserType.Admin));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Get_CustomMember_Success()
|
||||||
|
{
|
||||||
|
var (email, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||||
|
OrganizationUserType.Custom, new Permissions { AccessReports = true, ManageScim = true });
|
||||||
|
|
||||||
|
var response = await _client.GetAsync($"/public/members/{orgUser.Id}");
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<MemberResponseModel>();
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(email, result.Email);
|
||||||
|
|
||||||
|
Assert.Equal(OrganizationUserType.Custom, result.Type);
|
||||||
|
AssertHelper.AssertPropertyEqual(new PermissionsModel { AccessReports = true, ManageScim = true },
|
||||||
|
result.Permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(true, true)]
|
||||||
|
[BitAutoData(false, true)]
|
||||||
|
[BitAutoData(true, false)]
|
||||||
|
public async Task Get_CustomMember_WithDeprecatedPermissions_TreatsAsUser(bool editAssignedCollections, bool deleteAssignedCollections)
|
||||||
|
{
|
||||||
|
var (email, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||||
|
OrganizationUserType.Custom, new Permissions { EditAssignedCollections = editAssignedCollections, DeleteAssignedCollections = deleteAssignedCollections });
|
||||||
|
|
||||||
|
var response = await _client.GetAsync($"/public/members/{orgUser.Id}");
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<MemberResponseModel>();
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(email, result.Email);
|
||||||
|
|
||||||
|
Assert.Equal(OrganizationUserType.User, result.Type);
|
||||||
|
Assert.Null(result.Permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_CustomMember_Success()
|
||||||
|
{
|
||||||
|
var email = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||||
|
var request = new MemberCreateRequestModel
|
||||||
|
{
|
||||||
|
Email = email,
|
||||||
|
Type = OrganizationUserType.Custom,
|
||||||
|
ExternalId = "myCustomUser",
|
||||||
|
AccessAll = false,
|
||||||
|
Collections = [],
|
||||||
|
Groups = []
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _client.PostAsync("/public/members", JsonContent.Create(request));
|
||||||
|
|
||||||
|
// Assert against the response
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<MemberResponseModel>();
|
||||||
|
Assert.NotNull(result);
|
||||||
|
|
||||||
|
Assert.Equal(email, result.Email);
|
||||||
|
Assert.Equal(OrganizationUserType.Custom, result.Type);
|
||||||
|
Assert.Equal("myCustomUser", result.ExternalId);
|
||||||
|
Assert.False(result.AccessAll);
|
||||||
|
Assert.Empty(result.Collections);
|
||||||
|
|
||||||
|
// Assert against the database values
|
||||||
|
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||||
|
var orgUser = await organizationUserRepository.GetByIdAsync(result.Id);
|
||||||
|
|
||||||
|
Assert.Equal(email, orgUser.Email);
|
||||||
|
Assert.Equal(OrganizationUserType.Custom, orgUser.Type);
|
||||||
|
Assert.Equal("myCustomUser", orgUser.ExternalId);
|
||||||
|
Assert.False(orgUser.AccessAll);
|
||||||
|
Assert.Equal(OrganizationUserStatusType.Invited, orgUser.Status);
|
||||||
|
Assert.Equal(_organization.Id, orgUser.OrganizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Put_CustomMember_Success()
|
||||||
|
{
|
||||||
|
var (email, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||||
|
OrganizationUserType.User);
|
||||||
|
|
||||||
|
var request = new MemberUpdateRequestModel
|
||||||
|
{
|
||||||
|
Type = OrganizationUserType.Custom,
|
||||||
|
Permissions = new PermissionsModel
|
||||||
|
{
|
||||||
|
DeleteAnyCollection = true,
|
||||||
|
EditAnyCollection = true,
|
||||||
|
AccessEventLogs = true
|
||||||
|
},
|
||||||
|
AccessAll = false,
|
||||||
|
ExternalId = "example",
|
||||||
|
Collections = []
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _client.PutAsync($"/public/members/{orgUser.Id}", JsonContent.Create(request));
|
||||||
|
|
||||||
|
// Assert against the response
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<MemberResponseModel>();
|
||||||
|
Assert.NotNull(result);
|
||||||
|
|
||||||
|
Assert.Equal(email, result.Email);
|
||||||
|
Assert.Equal(OrganizationUserType.Custom, result.Type);
|
||||||
|
Assert.Equal("example", result.ExternalId);
|
||||||
|
AssertHelper.AssertPropertyEqual(
|
||||||
|
new PermissionsModel { DeleteAnyCollection = true, EditAnyCollection = true, AccessEventLogs = true },
|
||||||
|
result.Permissions);
|
||||||
|
Assert.False(result.AccessAll);
|
||||||
|
Assert.Empty(result.Collections);
|
||||||
|
|
||||||
|
// Assert against the database values
|
||||||
|
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||||
|
var updatedOrgUser = await organizationUserRepository.GetByIdAsync(result.Id);
|
||||||
|
|
||||||
|
Assert.Equal(OrganizationUserType.Custom, updatedOrgUser.Type);
|
||||||
|
Assert.Equal("example", updatedOrgUser.ExternalId);
|
||||||
|
Assert.False(updatedOrgUser.AccessAll);
|
||||||
|
Assert.Equal(OrganizationUserStatusType.Confirmed, updatedOrgUser.Status);
|
||||||
|
Assert.Equal(_organization.Id, updatedOrgUser.OrganizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Permissions property is optional and should not overwrite existing Permissions if not provided.
|
||||||
|
/// This is to preserve backwards compatibility with existing usage.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task Put_ExistingCustomMember_NullPermissions_DoesNotOverwritePermissions()
|
||||||
|
{
|
||||||
|
var (email, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||||
|
OrganizationUserType.Custom, new Permissions { CreateNewCollections = true, ManageScim = true, ManageGroups = true, ManageUsers = true });
|
||||||
|
|
||||||
|
var request = new MemberUpdateRequestModel
|
||||||
|
{
|
||||||
|
Type = OrganizationUserType.Custom,
|
||||||
|
AccessAll = false,
|
||||||
|
ExternalId = "example",
|
||||||
|
Collections = []
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _client.PutAsync($"/public/members/{orgUser.Id}", JsonContent.Create(request));
|
||||||
|
|
||||||
|
// Assert against the response
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<MemberResponseModel>();
|
||||||
|
Assert.NotNull(result);
|
||||||
|
|
||||||
|
Assert.Equal(OrganizationUserType.Custom, result.Type);
|
||||||
|
AssertHelper.AssertPropertyEqual(
|
||||||
|
new PermissionsModel { CreateNewCollections = true, ManageScim = true, ManageGroups = true, ManageUsers = true },
|
||||||
|
result.Permissions);
|
||||||
|
|
||||||
|
// Assert against the database values
|
||||||
|
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||||
|
var updatedOrgUser = await organizationUserRepository.GetByIdAsync(result.Id);
|
||||||
|
|
||||||
|
Assert.Equal(OrganizationUserType.Custom, updatedOrgUser.Type);
|
||||||
|
AssertHelper.AssertPropertyEqual(
|
||||||
|
new Permissions { CreateNewCollections = true, ManageScim = true, ManageGroups = true, ManageUsers = true },
|
||||||
|
orgUser.GetPermissions());
|
||||||
|
}
|
||||||
|
}
|
@ -73,4 +73,13 @@ public class ApiApplicationFactory : WebApplicationFactoryBase<Startup>
|
|||||||
{
|
{
|
||||||
return await _identityApplicationFactory.TokenFromAccessTokenAsync(clientId, clientSecret);
|
return await _identityApplicationFactory.TokenFromAccessTokenAsync(clientId, clientSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper for logging in with an Organization api key.
|
||||||
|
/// Currently used for the Public Api
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string> LoginWithOrganizationApiKeyAsync(string clientId, string clientSecret)
|
||||||
|
{
|
||||||
|
return await _identityApplicationFactory.TokenFromOrganizationApiKeyAsync(clientId, clientSecret);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
37
test/Api.IntegrationTest/Helpers/LoginHelper.cs
Normal file
37
test/Api.IntegrationTest/Helpers/LoginHelper.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using Bit.Api.IntegrationTest.Factories;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.IntegrationTestCommon.Factories;
|
||||||
|
|
||||||
|
namespace Bit.Api.IntegrationTest.Helpers;
|
||||||
|
|
||||||
|
public class LoginHelper
|
||||||
|
{
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
private readonly ApiApplicationFactory _factory;
|
||||||
|
|
||||||
|
public LoginHelper(ApiApplicationFactory factory, HttpClient client)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
_client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoginWithOrganizationApiKeyAsync(Guid organizationId)
|
||||||
|
{
|
||||||
|
var (clientId, apiKey) = await GetOrganizationApiKey(_factory, organizationId);
|
||||||
|
var token = await _factory.LoginWithOrganizationApiKeyAsync(clientId, apiKey);
|
||||||
|
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||||
|
_client.DefaultRequestHeaders.Add("client_id", clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(string clientId, string apiKey)> GetOrganizationApiKey<T>(
|
||||||
|
WebApplicationFactoryBase<T> factory,
|
||||||
|
Guid organizationId)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
var organizationApiKeyRepository = factory.GetService<IOrganizationApiKeyRepository>();
|
||||||
|
var apiKeys = await organizationApiKeyRepository.GetManyByOrganizationIdTypeAsync(organizationId);
|
||||||
|
var clientId = $"organization.{organizationId}";
|
||||||
|
return (clientId, apiKeys.SingleOrDefault().ApiKey);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Api.IntegrationTest.Factories;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.IntegrationTestCommon.Factories;
|
using Bit.IntegrationTestCommon.Factories;
|
||||||
@ -15,7 +17,9 @@ public static class OrganizationTestHelpers
|
|||||||
string ownerEmail = "integration-test@bitwarden.com",
|
string ownerEmail = "integration-test@bitwarden.com",
|
||||||
string name = "Integration Test Org",
|
string name = "Integration Test Org",
|
||||||
string billingEmail = "integration-test@bitwarden.com",
|
string billingEmail = "integration-test@bitwarden.com",
|
||||||
string ownerKey = "test-key") where T : class
|
string ownerKey = "test-key",
|
||||||
|
int passwordManagerSeats = 0,
|
||||||
|
PaymentMethodType paymentMethod = PaymentMethodType.None) where T : class
|
||||||
{
|
{
|
||||||
var userRepository = factory.GetService<IUserRepository>();
|
var userRepository = factory.GetService<IUserRepository>();
|
||||||
var organizationService = factory.GetService<IOrganizationService>();
|
var organizationService = factory.GetService<IOrganizationService>();
|
||||||
@ -29,17 +33,23 @@ public static class OrganizationTestHelpers
|
|||||||
Plan = plan,
|
Plan = plan,
|
||||||
OwnerKey = ownerKey,
|
OwnerKey = ownerKey,
|
||||||
Owner = owner,
|
Owner = owner,
|
||||||
|
AdditionalSeats = passwordManagerSeats,
|
||||||
|
PaymentMethodType = paymentMethod
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Tuple<Organization, OrganizationUser>(signUpResult.organization, signUpResult.organizationUser);
|
return new Tuple<Organization, OrganizationUser>(signUpResult.organization, signUpResult.organizationUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an OrganizationUser. The user account must already be created.
|
||||||
|
/// </summary>
|
||||||
public static async Task<OrganizationUser> CreateUserAsync<T>(
|
public static async Task<OrganizationUser> CreateUserAsync<T>(
|
||||||
WebApplicationFactoryBase<T> factory,
|
WebApplicationFactoryBase<T> factory,
|
||||||
Guid organizationId,
|
Guid organizationId,
|
||||||
string userEmail,
|
string userEmail,
|
||||||
OrganizationUserType type,
|
OrganizationUserType type,
|
||||||
bool accessSecretsManager = false
|
bool accessSecretsManager = false,
|
||||||
|
Permissions? permissions = null
|
||||||
) where T : class
|
) where T : class
|
||||||
{
|
{
|
||||||
var userRepository = factory.GetService<IUserRepository>();
|
var userRepository = factory.GetService<IUserRepository>();
|
||||||
@ -59,8 +69,36 @@ public static class OrganizationTestHelpers
|
|||||||
AccessSecretsManager = accessSecretsManager,
|
AccessSecretsManager = accessSecretsManager,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (permissions != null)
|
||||||
|
{
|
||||||
|
orgUser.SetPermissions(permissions);
|
||||||
|
}
|
||||||
|
|
||||||
await organizationUserRepository.CreateAsync(orgUser);
|
await organizationUserRepository.CreateAsync(orgUser);
|
||||||
|
|
||||||
return orgUser;
|
return orgUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new User account with a unique email address and a corresponding OrganizationUser for
|
||||||
|
/// the specified organization.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<(string, OrganizationUser)> CreateNewUserWithAccountAsync(
|
||||||
|
ApiApplicationFactory factory,
|
||||||
|
Guid organizationId,
|
||||||
|
OrganizationUserType userType,
|
||||||
|
Permissions? permissions = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var email = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
await factory.LoginWithNewAccount(email);
|
||||||
|
|
||||||
|
// Create organizationUser
|
||||||
|
var organizationUser = await OrganizationTestHelpers.CreateUserAsync(factory, organizationId, email, userType,
|
||||||
|
permissions: permissions);
|
||||||
|
|
||||||
|
return (email, organizationUser);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,7 +127,7 @@ public class OrganizationUsersControllerTests
|
|||||||
await sutProvider.Sut.Invite(organizationAbility.Id, model);
|
await sutProvider.Sut.Invite(organizationAbility.Id, model);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).InviteUsersAsync(organizationAbility.Id,
|
await sutProvider.GetDependency<IOrganizationService>().Received(1).InviteUsersAsync(organizationAbility.Id,
|
||||||
userId, Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(invites =>
|
userId, systemUser: null, Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(invites =>
|
||||||
invites.Count() == 1 &&
|
invites.Count() == 1 &&
|
||||||
invites.First().Item1.Emails.SequenceEqual(model.Emails) &&
|
invites.First().Item1.Emails.SequenceEqual(model.Emails) &&
|
||||||
invites.First().Item1.Type == model.Type &&
|
invites.First().Item1.Type == model.Type &&
|
||||||
|
@ -464,7 +464,7 @@ public class OrganizationServiceTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User,
|
[OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User,
|
||||||
InvitorUserType = OrganizationUserType.Owner), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
InvitorUserType = OrganizationUserType.Owner), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||||
public async Task InviteUser_NoEmails_Throws(Organization organization, OrganizationUser invitor,
|
public async Task InviteUsers_NoEmails_Throws(Organization organization, OrganizationUser invitor,
|
||||||
OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
invite.Emails = null;
|
invite.Emails = null;
|
||||||
@ -472,12 +472,12 @@ public class OrganizationServiceTests
|
|||||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
await Assert.ThrowsAsync<NotFoundException>(
|
await Assert.ThrowsAsync<NotFoundException>(
|
||||||
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, new (OrganizationUserInvite, string)[] { (invite, null) }));
|
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[OrganizationInviteCustomize, OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
[OrganizationInviteCustomize, OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||||
public async Task InviteUser_DuplicateEmails_PassesWithoutDuplicates(Organization organization, OrganizationUser invitor,
|
public async Task InviteUsers_DuplicateEmails_PassesWithoutDuplicates(Organization organization, OrganizationUser invitor,
|
||||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
@ -508,7 +508,7 @@ public class OrganizationServiceTests
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, new (OrganizationUserInvite, string)[] { (invite, null) });
|
await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) });
|
||||||
|
|
||||||
await sutProvider.GetDependency<IMailService>().Received(1)
|
await sutProvider.GetDependency<IMailService>().Received(1)
|
||||||
.SendOrganizationInviteEmailsAsync(Arg.Is<OrganizationInvitesInfo>(info =>
|
.SendOrganizationInviteEmailsAsync(Arg.Is<OrganizationInvitesInfo>(info =>
|
||||||
@ -520,7 +520,7 @@ public class OrganizationServiceTests
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[OrganizationInviteCustomize, OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
[OrganizationInviteCustomize, OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||||
public async Task InviteUser_SsoOrgWithNullSsoConfig_Passes(Organization organization, OrganizationUser invitor,
|
public async Task InviteUsers_SsoOrgWithNullSsoConfig_Passes(Organization organization, OrganizationUser invitor,
|
||||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
@ -557,19 +557,18 @@ public class OrganizationServiceTests
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, new (OrganizationUserInvite, string)[] { (invite, null) });
|
await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) });
|
||||||
|
|
||||||
await sutProvider.GetDependency<IMailService>().Received(1)
|
await sutProvider.GetDependency<IMailService>().Received(1)
|
||||||
.SendOrganizationInviteEmailsAsync(Arg.Is<OrganizationInvitesInfo>(info =>
|
.SendOrganizationInviteEmailsAsync(Arg.Is<OrganizationInvitesInfo>(info =>
|
||||||
info.OrgUserTokenPairs.Count() == invite.Emails.Distinct().Count() &&
|
info.OrgUserTokenPairs.Count() == invite.Emails.Distinct().Count() &&
|
||||||
info.IsFreeOrg == (organization.PlanType == PlanType.Free) &&
|
info.IsFreeOrg == (organization.PlanType == PlanType.Free) &&
|
||||||
info.OrganizationName == organization.Name));
|
info.OrganizationName == organization.Name));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[OrganizationInviteCustomize, OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
[OrganizationInviteCustomize, OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||||
public async Task InviteUser_SsoOrgWithNeverEnabledRequireSsoPolicy_Passes(Organization organization, SsoConfig ssoConfig, OrganizationUser invitor,
|
public async Task InviteUsers_SsoOrgWithNeverEnabledRequireSsoPolicy_Passes(Organization organization, SsoConfig ssoConfig, OrganizationUser invitor,
|
||||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
@ -608,8 +607,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) });
|
||||||
await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, new (OrganizationUserInvite, string)[] { (invite, null) });
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IMailService>().Received(1)
|
await sutProvider.GetDependency<IMailService>().Received(1)
|
||||||
.SendOrganizationInviteEmailsAsync(Arg.Is<OrganizationInvitesInfo>(info =>
|
.SendOrganizationInviteEmailsAsync(Arg.Is<OrganizationInvitesInfo>(info =>
|
||||||
@ -623,14 +621,14 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
InviteeUserType = OrganizationUserType.Admin,
|
InviteeUserType = OrganizationUserType.Admin,
|
||||||
InvitorUserType = OrganizationUserType.Owner
|
InvitorUserType = OrganizationUserType.Owner
|
||||||
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||||
public async Task InviteUser_NoOwner_Throws(Organization organization, OrganizationUser invitor,
|
public async Task InviteUsers_NoOwner_Throws(Organization organization, OrganizationUser invitor,
|
||||||
OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
|
||||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, new (OrganizationUserInvite, string)[] { (invite, null) }));
|
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }));
|
||||||
Assert.Contains("Organization must have at least one confirmed owner.", exception.Message);
|
Assert.Contains("Organization must have at least one confirmed owner.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -639,7 +637,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
InviteeUserType = OrganizationUserType.Owner,
|
InviteeUserType = OrganizationUserType.Owner,
|
||||||
InvitorUserType = OrganizationUserType.Admin
|
InvitorUserType = OrganizationUserType.Admin
|
||||||
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||||
public async Task InviteUser_NonOwnerConfiguringOwner_Throws(Organization organization, OrganizationUserInvite invite,
|
public async Task InviteUsers_NonOwnerConfiguringOwner_Throws(Organization organization, OrganizationUserInvite invite,
|
||||||
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
@ -649,7 +647,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
currentContext.OrganizationAdmin(organization.Id).Returns(true);
|
currentContext.OrganizationAdmin(organization.Id).Returns(true);
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, new (OrganizationUserInvite, string)[] { (invite, null) }));
|
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }));
|
||||||
Assert.Contains("only an owner", exception.Message.ToLowerInvariant());
|
Assert.Contains("only an owner", exception.Message.ToLowerInvariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -658,7 +656,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
InviteeUserType = OrganizationUserType.Custom,
|
InviteeUserType = OrganizationUserType.Custom,
|
||||||
InvitorUserType = OrganizationUserType.User
|
InvitorUserType = OrganizationUserType.User
|
||||||
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||||
public async Task InviteUser_NonAdminConfiguringAdmin_Throws(Organization organization, OrganizationUserInvite invite,
|
public async Task InviteUsers_NonAdminConfiguringAdmin_Throws(Organization organization, OrganizationUserInvite invite,
|
||||||
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
organization.UseCustomPermissions = true;
|
organization.UseCustomPermissions = true;
|
||||||
@ -670,7 +668,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
currentContext.OrganizationUser(organization.Id).Returns(true);
|
currentContext.OrganizationUser(organization.Id).Returns(true);
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, new (OrganizationUserInvite, string)[] { (invite, null) }));
|
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }));
|
||||||
Assert.Contains("your account does not have permission to manage users", exception.Message.ToLowerInvariant());
|
Assert.Contains("your account does not have permission to manage users", exception.Message.ToLowerInvariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -679,7 +677,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
InviteeUserType = OrganizationUserType.Custom,
|
InviteeUserType = OrganizationUserType.Custom,
|
||||||
InvitorUserType = OrganizationUserType.Admin
|
InvitorUserType = OrganizationUserType.Admin
|
||||||
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||||
public async Task InviteUser_WithCustomType_WhenUseCustomPermissionsIsFalse_Throws(Organization organization, OrganizationUserInvite invite,
|
public async Task InviteUsers_WithCustomType_WhenUseCustomPermissionsIsFalse_Throws(Organization organization, OrganizationUserInvite invite,
|
||||||
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
organization.UseCustomPermissions = false;
|
organization.UseCustomPermissions = false;
|
||||||
@ -697,7 +695,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
currentContext.ManageUsers(organization.Id).Returns(true);
|
currentContext.ManageUsers(organization.Id).Returns(true);
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, new (OrganizationUserInvite, string)[] { (invite, null) }));
|
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }));
|
||||||
Assert.Contains("to enable custom permissions", exception.Message.ToLowerInvariant());
|
Assert.Contains("to enable custom permissions", exception.Message.ToLowerInvariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -706,7 +704,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
InviteeUserType = OrganizationUserType.Custom,
|
InviteeUserType = OrganizationUserType.Custom,
|
||||||
InvitorUserType = OrganizationUserType.Admin
|
InvitorUserType = OrganizationUserType.Admin
|
||||||
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||||
public async Task InviteUser_WithCustomType_WhenUseCustomPermissionsIsTrue_Passes(Organization organization, OrganizationUserInvite invite,
|
public async Task InviteUsers_WithCustomType_WhenUseCustomPermissionsIsTrue_Passes(Organization organization, OrganizationUserInvite invite,
|
||||||
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
organization.Seats = 10;
|
organization.Seats = 10;
|
||||||
@ -727,7 +725,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
currentContext.OrganizationOwner(organization.Id).Returns(true);
|
currentContext.OrganizationOwner(organization.Id).Returns(true);
|
||||||
currentContext.ManageUsers(organization.Id).Returns(true);
|
currentContext.ManageUsers(organization.Id).Returns(true);
|
||||||
|
|
||||||
await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, new (OrganizationUserInvite, string)[] { (invite, null) });
|
await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) });
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -736,7 +734,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
[BitAutoData(OrganizationUserType.Manager)]
|
[BitAutoData(OrganizationUserType.Manager)]
|
||||||
[BitAutoData(OrganizationUserType.Owner)]
|
[BitAutoData(OrganizationUserType.Owner)]
|
||||||
[BitAutoData(OrganizationUserType.User)]
|
[BitAutoData(OrganizationUserType.User)]
|
||||||
public async Task InviteUser_WithNonCustomType_WhenUseCustomPermissionsIsFalse_Passes(OrganizationUserType inviteUserType, Organization organization, OrganizationUserInvite invite,
|
public async Task InviteUsers_WithNonCustomType_WhenUseCustomPermissionsIsFalse_Passes(OrganizationUserType inviteUserType, Organization organization, OrganizationUserInvite invite,
|
||||||
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
organization.Seats = 10;
|
organization.Seats = 10;
|
||||||
@ -758,7 +756,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
currentContext.OrganizationOwner(organization.Id).Returns(true);
|
currentContext.OrganizationOwner(organization.Id).Returns(true);
|
||||||
currentContext.ManageUsers(organization.Id).Returns(true);
|
currentContext.ManageUsers(organization.Id).Returns(true);
|
||||||
|
|
||||||
await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, new (OrganizationUserInvite, string)[] { (invite, null) });
|
await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) });
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -766,7 +764,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
InviteeUserType = OrganizationUserType.Manager,
|
InviteeUserType = OrganizationUserType.Manager,
|
||||||
InvitorUserType = OrganizationUserType.Custom
|
InvitorUserType = OrganizationUserType.Custom
|
||||||
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||||
public async Task InviteUser_CustomUserWithoutManageUsersConfiguringUser_Throws(Organization organization, OrganizationUserInvite invite,
|
public async Task InviteUsers_CustomUserWithoutManageUsersConfiguringUser_Throws(Organization organization, OrganizationUserInvite invite,
|
||||||
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = false },
|
invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = false },
|
||||||
@ -785,7 +783,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
currentContext.ManageUsers(organization.Id).Returns(false);
|
currentContext.ManageUsers(organization.Id).Returns(false);
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, new (OrganizationUserInvite, string)[] { (invite, null) }));
|
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }));
|
||||||
Assert.Contains("account does not have permission", exception.Message.ToLowerInvariant());
|
Assert.Contains("account does not have permission", exception.Message.ToLowerInvariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -794,7 +792,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
InviteeUserType = OrganizationUserType.Admin,
|
InviteeUserType = OrganizationUserType.Admin,
|
||||||
InvitorUserType = OrganizationUserType.Custom
|
InvitorUserType = OrganizationUserType.Custom
|
||||||
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||||
public async Task InviteUser_CustomUserConfiguringAdmin_Throws(Organization organization, OrganizationUserInvite invite,
|
public async Task InviteUsers_CustomUserConfiguringAdmin_Throws(Organization organization, OrganizationUserInvite invite,
|
||||||
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = true },
|
invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = true },
|
||||||
@ -811,7 +809,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
currentContext.ManageUsers(organization.Id).Returns(true);
|
currentContext.ManageUsers(organization.Id).Returns(true);
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, new (OrganizationUserInvite, string)[] { (invite, null) }));
|
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }));
|
||||||
Assert.Contains("can not manage admins", exception.Message.ToLowerInvariant());
|
Assert.Contains("can not manage admins", exception.Message.ToLowerInvariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -820,7 +818,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
InviteeUserType = OrganizationUserType.User,
|
InviteeUserType = OrganizationUserType.User,
|
||||||
InvitorUserType = OrganizationUserType.Owner
|
InvitorUserType = OrganizationUserType.Owner
|
||||||
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||||
public async Task InviteUser_NoPermissionsObject_Passes(Organization organization, OrganizationUserInvite invite,
|
public async Task InviteUsers_NoPermissionsObject_Passes(Organization organization, OrganizationUserInvite invite,
|
||||||
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
invite.Permissions = null;
|
invite.Permissions = null;
|
||||||
@ -838,7 +836,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
currentContext.OrganizationOwner(organization.Id).Returns(true);
|
currentContext.OrganizationOwner(organization.Id).Returns(true);
|
||||||
currentContext.ManageUsers(organization.Id).Returns(true);
|
currentContext.ManageUsers(organization.Id).Returns(true);
|
||||||
|
|
||||||
await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, new (OrganizationUserInvite, string)[] { (invite, null) });
|
await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) });
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -846,28 +844,132 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
InviteeUserType = OrganizationUserType.User,
|
InviteeUserType = OrganizationUserType.User,
|
||||||
InvitorUserType = OrganizationUserType.Custom
|
InvitorUserType = OrganizationUserType.Custom
|
||||||
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||||
public async Task InviteUser_Passes(Organization organization, IEnumerable<(OrganizationUserInvite invite, string externalId)> invites,
|
public async Task InviteUser_Passes(Organization organization, OrganizationUserInvite invite, string externalId,
|
||||||
OrganizationUser invitor,
|
OrganizationUser invitor,
|
||||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
SutProvider<OrganizationService> sutProvider)
|
SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
|
// This method is only used to invite 1 user at a time
|
||||||
|
invite.Emails = new[] { invite.Emails.First() };
|
||||||
|
|
||||||
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks
|
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks
|
||||||
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
|
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
|
||||||
sutProvider.Create();
|
sutProvider.Create();
|
||||||
|
|
||||||
invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = true },
|
InviteUser_ArrangeCurrentContextPermissions(organization, sutProvider);
|
||||||
new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
});
|
|
||||||
|
|
||||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
var currentContext = sutProvider.GetDependency<ICurrentContext>();
|
|
||||||
|
|
||||||
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
|
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
|
||||||
organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner)
|
organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner)
|
||||||
.Returns(new[] { owner });
|
.Returns(new[] { owner });
|
||||||
|
|
||||||
|
// Mock tokenable factory to return a token that expires in 5 days
|
||||||
|
sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()
|
||||||
|
.CreateToken(Arg.Any<OrganizationUser>())
|
||||||
|
.Returns(
|
||||||
|
info => new OrgUserInviteTokenable(info.Arg<OrganizationUser>())
|
||||||
|
{
|
||||||
|
ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
|
||||||
|
SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository);
|
||||||
|
|
||||||
|
await sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, systemUser: null, invite, externalId);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IMailService>().Received(1)
|
||||||
|
.SendOrganizationInviteEmailsAsync(Arg.Is<OrganizationInvitesInfo>(info =>
|
||||||
|
info.OrgUserTokenPairs.Count() == 1 &&
|
||||||
|
info.IsFreeOrg == (organization.PlanType == PlanType.Free) &&
|
||||||
|
info.OrganizationName == organization.Name));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[OrganizationInviteCustomize(
|
||||||
|
InviteeUserType = OrganizationUserType.User,
|
||||||
|
InvitorUserType = OrganizationUserType.Custom
|
||||||
|
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||||
|
public async Task InviteUser_InvitingMoreThanOneUser_Throws(Organization organization, OrganizationUserInvite invite, string externalId,
|
||||||
|
OrganizationUser invitor,
|
||||||
|
SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, systemUser: null, invite, externalId));
|
||||||
|
Assert.Contains("This method can only be used to invite a single user.", exception.Message);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IMailService>().DidNotReceiveWithAnyArgs()
|
||||||
|
.SendOrganizationInviteEmailsAsync(default);
|
||||||
|
await sutProvider.GetDependency<IEventService>().DidNotReceive()
|
||||||
|
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, EventSystemUser, DateTime?)>>());
|
||||||
|
await sutProvider.GetDependency<IEventService>().DidNotReceive()
|
||||||
|
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[OrganizationInviteCustomize(
|
||||||
|
InviteeUserType = OrganizationUserType.User,
|
||||||
|
InvitorUserType = OrganizationUserType.Custom
|
||||||
|
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||||
|
public async Task InviteUser_UserAlreadyInvited_Throws(Organization organization, OrganizationUserInvite invite, string externalId,
|
||||||
|
OrganizationUser invitor,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
// This method is only used to invite 1 user at a time
|
||||||
|
invite.Emails = new[] { invite.Emails.First() };
|
||||||
|
|
||||||
|
// The user has already been invited
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.SelectKnownEmailsAsync(organization.Id, Arg.Any<IEnumerable<string>>(), false)
|
||||||
|
.Returns(new List<string> { invite.Emails.First() });
|
||||||
|
|
||||||
|
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks
|
||||||
|
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
|
||||||
|
sutProvider.Create();
|
||||||
|
|
||||||
|
InviteUser_ArrangeCurrentContextPermissions(organization, sutProvider);
|
||||||
|
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner)
|
||||||
|
.Returns(new[] { owner });
|
||||||
|
|
||||||
|
// Mock tokenable factory to return a token that expires in 5 days
|
||||||
|
sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()
|
||||||
|
.CreateToken(Arg.Any<OrganizationUser>())
|
||||||
|
.Returns(
|
||||||
|
info => new OrgUserInviteTokenable(info.Arg<OrganizationUser>())
|
||||||
|
{
|
||||||
|
ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
|
||||||
|
SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut
|
||||||
|
.InviteUserAsync(organization.Id, invitor.UserId, systemUser: null, invite, externalId));
|
||||||
|
Assert.Contains("This user has already been invited", exception.Message);
|
||||||
|
|
||||||
|
// MailService and EventService are still called, but with no OrgUsers
|
||||||
|
await sutProvider.GetDependency<IMailService>().Received(1)
|
||||||
|
.SendOrganizationInviteEmailsAsync(Arg.Is<OrganizationInvitesInfo>(info =>
|
||||||
|
!info.OrgUserTokenPairs.Any() &&
|
||||||
|
info.IsFreeOrg == (organization.PlanType == PlanType.Free) &&
|
||||||
|
info.OrganizationName == organization.Name));
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||||
|
.LogOrganizationUserEventsAsync(Arg.Is<IEnumerable<(OrganizationUser, EventType, DateTime?)>>(events => !events.Any()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InviteUser_ArrangeCurrentContextPermissions(Organization organization, SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
var currentContext = sutProvider.GetDependency<ICurrentContext>();
|
||||||
currentContext.ManageUsers(organization.Id).Returns(true);
|
currentContext.ManageUsers(organization.Id).Returns(true);
|
||||||
currentContext.AccessReports(organization.Id).Returns(true);
|
currentContext.AccessReports(organization.Id).Returns(true);
|
||||||
currentContext.ManageGroups(organization.Id).Returns(true);
|
currentContext.ManageGroups(organization.Id).Returns(true);
|
||||||
@ -889,6 +991,30 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
DeleteAnyCollection = true
|
DeleteAnyCollection = true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[OrganizationInviteCustomize(
|
||||||
|
InviteeUserType = OrganizationUserType.User,
|
||||||
|
InvitorUserType = OrganizationUserType.Custom
|
||||||
|
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||||
|
public async Task InviteUsers_Passes(Organization organization, IEnumerable<(OrganizationUserInvite invite, string externalId)> invites,
|
||||||
|
OrganizationUser invitor,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks
|
||||||
|
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
|
||||||
|
sutProvider.Create();
|
||||||
|
|
||||||
|
InviteUser_ArrangeCurrentContextPermissions(organization, sutProvider);
|
||||||
|
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner)
|
||||||
|
.Returns(new[] { owner });
|
||||||
|
|
||||||
// Mock tokenable factory to return a token that expires in 5 days
|
// Mock tokenable factory to return a token that expires in 5 days
|
||||||
sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()
|
sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()
|
||||||
@ -903,7 +1029,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
|
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
|
||||||
SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository);
|
SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository);
|
||||||
|
|
||||||
await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, invites);
|
await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, invites);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IMailService>().Received(1)
|
await sutProvider.GetDependency<IMailService>().Received(1)
|
||||||
.SendOrganizationInviteEmailsAsync(Arg.Is<OrganizationInvitesInfo>(info =>
|
.SendOrganizationInviteEmailsAsync(Arg.Is<OrganizationInvitesInfo>(info =>
|
||||||
@ -919,7 +1045,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
InviteeUserType = OrganizationUserType.User,
|
InviteeUserType = OrganizationUserType.User,
|
||||||
InvitorUserType = OrganizationUserType.Custom
|
InvitorUserType = OrganizationUserType.Custom
|
||||||
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
), OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||||
public async Task InviteUser_WithEventSystemUser_Passes(Organization organization, EventSystemUser eventSystemUser, IEnumerable<(OrganizationUserInvite invite, string externalId)> invites,
|
public async Task InviteUsers_WithEventSystemUser_Passes(Organization organization, EventSystemUser eventSystemUser, IEnumerable<(OrganizationUserInvite invite, string externalId)> invites,
|
||||||
OrganizationUser invitor,
|
OrganizationUser invitor,
|
||||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
SutProvider<OrganizationService> sutProvider)
|
SutProvider<OrganizationService> sutProvider)
|
||||||
@ -957,7 +1083,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
await sutProvider.Sut.InviteUsersAsync(organization.Id, eventSystemUser, invites);
|
await sutProvider.Sut.InviteUsersAsync(organization.Id, invitingUserId: null, eventSystemUser, invites);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IMailService>().Received(1)
|
await sutProvider.GetDependency<IMailService>().Received(1)
|
||||||
.SendOrganizationInviteEmailsAsync(Arg.Is<OrganizationInvitesInfo>(info =>
|
.SendOrganizationInviteEmailsAsync(Arg.Is<OrganizationInvitesInfo>(info =>
|
||||||
@ -969,7 +1095,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData, OrganizationCustomize(FlexibleCollections = false), OrganizationInviteCustomize]
|
[Theory, BitAutoData, OrganizationCustomize(FlexibleCollections = false), OrganizationInviteCustomize]
|
||||||
public async Task InviteUser_WithSecretsManager_Passes(Organization organization,
|
public async Task InviteUsers_WithSecretsManager_Passes(Organization organization,
|
||||||
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites,
|
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites,
|
||||||
OrganizationUser savingUser, SutProvider<OrganizationService> sutProvider)
|
OrganizationUser savingUser, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
@ -992,7 +1118,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
|
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
|
||||||
SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository);
|
SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository);
|
||||||
|
|
||||||
await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, invites);
|
await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, systemUser: null, invites);
|
||||||
|
|
||||||
sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>().Received(1)
|
sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>().Received(1)
|
||||||
.UpdateSubscriptionAsync(Arg.Is<SecretsManagerSubscriptionUpdate>(update =>
|
.UpdateSubscriptionAsync(Arg.Is<SecretsManagerSubscriptionUpdate>(update =>
|
||||||
@ -1003,7 +1129,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData, OrganizationCustomize(FlexibleCollections = false), OrganizationInviteCustomize]
|
[Theory, BitAutoData, OrganizationCustomize(FlexibleCollections = false), OrganizationInviteCustomize]
|
||||||
public async Task InviteUser_WithSecretsManager_WhenErrorIsThrown_RevertsAutoscaling(Organization organization,
|
public async Task InviteUsers_WithSecretsManager_WhenErrorIsThrown_RevertsAutoscaling(Organization organization,
|
||||||
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites,
|
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites,
|
||||||
OrganizationUser savingUser, SutProvider<OrganizationService> sutProvider)
|
OrganizationUser savingUser, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
@ -1030,7 +1156,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
// Throw error at the end of the try block
|
// Throw error at the end of the try block
|
||||||
sutProvider.GetDependency<IReferenceEventService>().RaiseEventAsync(default).ThrowsForAnyArgs<BadRequestException>();
|
sutProvider.GetDependency<IReferenceEventService>().RaiseEventAsync(default).ThrowsForAnyArgs<BadRequestException>();
|
||||||
|
|
||||||
await Assert.ThrowsAsync<AggregateException>(async () => await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, invites));
|
await Assert.ThrowsAsync<AggregateException>(async () => await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, systemUser: null, invites));
|
||||||
|
|
||||||
// OrgUser is reverted
|
// OrgUser is reverted
|
||||||
// Note: we don't know what their guids are so comparing length is the best we can do
|
// Note: we don't know what their guids are so comparing length is the best we can do
|
||||||
@ -1059,7 +1185,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, OrganizationCustomize(FlexibleCollections = true), BitAutoData]
|
[Theory, OrganizationCustomize(FlexibleCollections = true), BitAutoData]
|
||||||
public async Task InviteUser_WithFlexibleCollections_WhenInvitingManager_Throws(Organization organization,
|
public async Task InviteUsers_WithFlexibleCollections_WhenInvitingManager_Throws(Organization organization,
|
||||||
OrganizationUserInvite invite, OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
OrganizationUserInvite invite, OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
invite.Type = OrganizationUserType.Manager;
|
invite.Type = OrganizationUserType.Manager;
|
||||||
@ -1074,14 +1200,14 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
.Returns(true);
|
.Returns(true);
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId,
|
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null,
|
||||||
new (OrganizationUserInvite, string)[] { (invite, null) }));
|
new (OrganizationUserInvite, string)[] { (invite, null) }));
|
||||||
|
|
||||||
Assert.Contains("manager role has been deprecated", exception.Message.ToLowerInvariant());
|
Assert.Contains("manager role has been deprecated", exception.Message.ToLowerInvariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, OrganizationCustomize(FlexibleCollections = true), BitAutoData]
|
[Theory, OrganizationCustomize(FlexibleCollections = true), BitAutoData]
|
||||||
public async Task InviteUser_WithFlexibleCollections_WithAccessAll_Throws(Organization organization,
|
public async Task InviteUsers_WithFlexibleCollections_WithAccessAll_Throws(Organization organization,
|
||||||
OrganizationUserInvite invite, OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
OrganizationUserInvite invite, OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
invite.Type = OrganizationUserType.User;
|
invite.Type = OrganizationUserType.User;
|
||||||
@ -1096,7 +1222,7 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
.Returns(true);
|
.Returns(true);
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId,
|
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null,
|
||||||
new (OrganizationUserInvite, string)[] { (invite, null) }));
|
new (OrganizationUserInvite, string)[] { (invite, null) }));
|
||||||
|
|
||||||
Assert.Contains("accessall property has been deprecated", exception.Message.ToLowerInvariant());
|
Assert.Contains("accessall property has been deprecated", exception.Message.ToLowerInvariant());
|
||||||
|
@ -61,4 +61,23 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase<Startup>
|
|||||||
|
|
||||||
return root.GetProperty("access_token").GetString();
|
return root.GetProperty("access_token").GetString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string> TokenFromOrganizationApiKeyAsync(string clientId, string clientSecret,
|
||||||
|
DeviceType deviceType = DeviceType.FirefoxBrowser)
|
||||||
|
{
|
||||||
|
var context = await Server.PostAsync("/connect/token",
|
||||||
|
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "scope", "api.organization" },
|
||||||
|
{ "client_id", clientId },
|
||||||
|
{ "client_secret", clientSecret },
|
||||||
|
{ "grant_type", "client_credentials" },
|
||||||
|
{ "deviceType", ((int)deviceType).ToString() }
|
||||||
|
}));
|
||||||
|
|
||||||
|
using var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||||
|
var root = body.RootElement;
|
||||||
|
|
||||||
|
return root.GetProperty("access_token").GetString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -166,6 +166,11 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
|||||||
|
|
||||||
// Disable logs
|
// Disable logs
|
||||||
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
|
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
|
||||||
|
|
||||||
|
// Noop StripePaymentService - this could be changed to integrate with our Stripe test account
|
||||||
|
var stripePaymentService = services.First(sd => sd.ServiceType == typeof(IPaymentService));
|
||||||
|
services.Remove(stripePaymentService);
|
||||||
|
services.AddSingleton(Substitute.For<IPaymentService>());
|
||||||
});
|
});
|
||||||
|
|
||||||
foreach (var configureTestService in _configureTestServices)
|
foreach (var configureTestService in _configureTestServices)
|
||||||
|
Loading…
Reference in New Issue
Block a user