1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00

Defect/SG-992 ProviderOrgs Missing Plan Type & EC-591/SG-996 - Provider Org Autoscaling Email Invites Working (#2596)

* SG-992 - Provider receives free org prompt when trying to auto scale org seats because plan type was missing and defaulting to free. PlanType has now been added to provider orgs returned as part of the profile sync.

* SG-992 - Updated Stored proc name to match convention

* EC-591 / SG-996 - (1) Update ProviderUserRepo.GetManyDetailsByProviderAsync to accept optional ProviderUserStatusType (2) Update OrganizationService.cs autoscaling user logic to check if an org is a provider org and send owner emails to the confirmed provider users instead of the managed org owners. Prevents scenario where newly created, managed orgs would not have an owner yet, and ownerEmails would be null and the email service would explode.

* EC-591 / SG-996 - Remove comments

* EC-591 / SG-996 - ES lint fix.

* SG-996 - SQL files must have SQL extensions.

* SG-996 / EC-591 - Update alter sql to be actually backwards compatible

* SG-996 - Make Status actually optional and backwards compatible for ProviderUserUserDetails_ReadByProvider.sql

* SG-992 - Update migrations to meet standards - (1) use CREATE OR ALTER and (2) Update view metadata after change if necessary

* EC-591 / SG-996 - Update Stored Proc migration to use proper standards: (1) Remove unnecessary code and (2) Use CREATE OR ALTER instead of just ALTER

* SG-992 / EC-591 / SG-996 - Refactor separate migrations into single migrations file per PR feedback

* SG-992/SG-996 - Add SyncControllerTests.cs with basic test suite + specific test suite to ensure provider orgs have plan type mapped to output product type properly.

* Fix lint issues by removing unnecessary using statements

* SG-992 - Refresh of view metadata has to target the stored procs that reference the view -- not the view itself.
This commit is contained in:
Jared Snider 2023-01-26 11:51:26 -05:00 committed by GitHub
parent 6dfbd06e8f
commit b412a01d2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 476 additions and 20 deletions

View File

@ -1,5 +1,6 @@
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Api.Models.Response; namespace Bit.Api.Models.Response;
@ -39,5 +40,6 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
UserId = organization.UserId?.ToString(); UserId = organization.UserId?.ToString();
ProviderId = organization.ProviderId?.ToString(); ProviderId = organization.ProviderId?.ToString();
ProviderName = organization.ProviderName; ProviderName = organization.ProviderName;
PlanProductType = StaticStore.GetPlan(organization.PlanType).Product;
} }
} }

View File

@ -34,4 +34,5 @@ public class ProviderUserOrganizationDetails
public Guid? ProviderId { get; set; } public Guid? ProviderId { get; set; }
public Guid? ProviderUserId { get; set; } public Guid? ProviderUserId { get; set; }
public string ProviderName { get; set; } public string ProviderName { get; set; }
public Enums.PlanType PlanType { get; set; }
} }

View File

@ -11,7 +11,7 @@ public interface IProviderUserRepository : IRepository<ProviderUser, Guid>
Task<ICollection<ProviderUser>> GetManyByUserAsync(Guid userId); Task<ICollection<ProviderUser>> GetManyByUserAsync(Guid userId);
Task<ProviderUser> GetByProviderUserAsync(Guid providerId, Guid userId); Task<ProviderUser> GetByProviderUserAsync(Guid providerId, Guid userId);
Task<ICollection<ProviderUser>> GetManyByProviderAsync(Guid providerId, ProviderUserType? type = null); Task<ICollection<ProviderUser>> GetManyByProviderAsync(Guid providerId, ProviderUserType? type = null);
Task<ICollection<ProviderUserUserDetails>> GetManyDetailsByProviderAsync(Guid providerId); Task<ICollection<ProviderUserUserDetails>> GetManyDetailsByProviderAsync(Guid providerId, ProviderUserStatusType? status = null);
Task<ICollection<ProviderUserProviderDetails>> GetManyDetailsByUserAsync(Guid userId, Task<ICollection<ProviderUserProviderDetails>> GetManyDetailsByUserAsync(Guid userId,
ProviderUserStatusType? status = null); ProviderUserStatusType? status = null);
Task<IEnumerable<ProviderUserOrganizationDetails>> GetManyOrganizationDetailsByUserAsync(Guid userId, ProviderUserStatusType? status = null); Task<IEnumerable<ProviderUserOrganizationDetails>> GetManyOrganizationDetailsByUserAsync(Guid userId, ProviderUserStatusType? status = null);

View File

@ -2,6 +2,7 @@
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Enums.Provider;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
@ -43,6 +44,8 @@ public class OrganizationService : IOrganizationService
private readonly IOrganizationConnectionRepository _organizationConnectionRepository; private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly ILogger<OrganizationService> _logger; private readonly ILogger<OrganizationService> _logger;
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IProviderUserRepository _providerUserRepository;
public OrganizationService( public OrganizationService(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
@ -69,7 +72,9 @@ public class OrganizationService : IOrganizationService
IOrganizationApiKeyRepository organizationApiKeyRepository, IOrganizationApiKeyRepository organizationApiKeyRepository,
IOrganizationConnectionRepository organizationConnectionRepository, IOrganizationConnectionRepository organizationConnectionRepository,
ICurrentContext currentContext, ICurrentContext currentContext,
ILogger<OrganizationService> logger) ILogger<OrganizationService> logger,
IProviderOrganizationRepository providerOrganizationRepository,
IProviderUserRepository providerUserRepository)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -96,6 +101,8 @@ public class OrganizationService : IOrganizationService
_organizationConnectionRepository = organizationConnectionRepository; _organizationConnectionRepository = organizationConnectionRepository;
_currentContext = currentContext; _currentContext = currentContext;
_logger = logger; _logger = logger;
_providerOrganizationRepository = providerOrganizationRepository;
_providerUserRepository = providerUserRepository;
} }
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
@ -1635,8 +1642,19 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException(failureMessage); throw new BadRequestException(failureMessage);
} }
var ownerEmails = (await _organizationUserRepository.GetManyByMinimumRoleAsync(organization.Id, var providerOrg = await this._providerOrganizationRepository.GetByOrganizationId(organization.Id);
OrganizationUserType.Owner)).Select(u => u.Email).Distinct();
IEnumerable<string> ownerEmails;
if (providerOrg != null)
{
ownerEmails = (await _providerUserRepository.GetManyDetailsByProviderAsync(providerOrg.ProviderId, ProviderUserStatusType.Confirmed))
.Select(u => u.Email).Distinct();
}
else
{
ownerEmails = (await _organizationUserRepository.GetManyByMinimumRoleAsync(organization.Id,
OrganizationUserType.Owner)).Select(u => u.Email).Distinct();
}
var initialSeatCount = organization.Seats.Value; var initialSeatCount = organization.Seats.Value;
await AdjustSeatsAsync(organization, seatsToAdd, prorationDate, ownerEmails); await AdjustSeatsAsync(organization, seatsToAdd, prorationDate, ownerEmails);

View File

@ -84,13 +84,13 @@ public class ProviderUserRepository : Repository<ProviderUser, Guid>, IProviderU
} }
} }
public async Task<ICollection<ProviderUserUserDetails>> GetManyDetailsByProviderAsync(Guid providerId) public async Task<ICollection<ProviderUserUserDetails>> GetManyDetailsByProviderAsync(Guid providerId, ProviderUserStatusType? status)
{ {
using (var connection = new SqlConnection(ConnectionString)) using (var connection = new SqlConnection(ConnectionString))
{ {
var results = await connection.QueryAsync<ProviderUserUserDetails>( var results = await connection.QueryAsync<ProviderUserUserDetails>(
"[dbo].[ProviderUserUserDetails_ReadByProviderId]", "[dbo].[ProviderUserUserDetails_ReadByProviderId]",
new { ProviderId = providerId }, new { ProviderId = providerId, Status = status },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
return results.ToList(); return results.ToList();

View File

@ -103,7 +103,7 @@ public class ProviderUserRepository :
return await query.FirstOrDefaultAsync(); return await query.FirstOrDefaultAsync();
} }
} }
public async Task<ICollection<ProviderUserUserDetails>> GetManyDetailsByProviderAsync(Guid providerId) public async Task<ICollection<ProviderUserUserDetails>> GetManyDetailsByProviderAsync(Guid providerId, ProviderUserStatusType? status)
{ {
using (var scope = ServiceScopeFactory.CreateScope()) using (var scope = ServiceScopeFactory.CreateScope())
{ {
@ -113,17 +113,19 @@ public class ProviderUserRepository :
on pu.UserId equals u.Id into u_g on pu.UserId equals u.Id into u_g
from u in u_g.DefaultIfEmpty() from u in u_g.DefaultIfEmpty()
select new { pu, u }; select new { pu, u };
var data = await view.Where(e => e.pu.ProviderId == providerId).Select(e => new ProviderUserUserDetails var data = await view
{ .Where(e => e.pu.ProviderId == providerId && (status == null || e.pu.Status == status))
Id = e.pu.Id, .Select(e => new ProviderUserUserDetails
UserId = e.pu.UserId, {
ProviderId = e.pu.ProviderId, Id = e.pu.Id,
Name = e.u.Name, UserId = e.pu.UserId,
Email = e.u.Email ?? e.pu.Email, ProviderId = e.pu.ProviderId,
Status = e.pu.Status, Name = e.u.Name,
Type = e.pu.Type, Email = e.u.Email ?? e.pu.Email,
Permissions = e.pu.Permissions, Status = e.pu.Status,
}).ToArrayAsync(); Type = e.pu.Type,
Permissions = e.pu.Permissions,
}).ToArrayAsync();
return data; return data;
} }
} }

View File

@ -41,6 +41,7 @@ public class ProviderUserOrganizationDetailsViewQuery : IQuery<ProviderUserOrgan
PrivateKey = x.o.PrivateKey, PrivateKey = x.o.PrivateKey,
ProviderId = x.p.Id, ProviderId = x.p.Id,
ProviderName = x.p.Name, ProviderName = x.p.Name,
PlanType = x.o.PlanType
}); });
} }
} }

View File

@ -1,5 +1,6 @@
CREATE PROCEDURE [dbo].[ProviderUserUserDetails_ReadByProviderId] CREATE PROCEDURE [dbo].[ProviderUserUserDetails_ReadByProviderId]
@ProviderId UNIQUEIDENTIFIER @ProviderId UNIQUEIDENTIFIER,
@Status TINYINT = NULL
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON
@ -10,4 +11,5 @@ BEGIN
[dbo].[ProviderUserUserDetailsView] [dbo].[ProviderUserUserDetailsView]
WHERE WHERE
[ProviderId] = @ProviderId [ProviderId] = @ProviderId
AND [Status] = COALESCE(@Status, [Status])
END END

View File

@ -30,7 +30,8 @@ SELECT
PU.[Type], PU.[Type],
PO.[ProviderId], PO.[ProviderId],
PU.[Id] ProviderUserId, PU.[Id] ProviderUserId,
P.[Name] ProviderName P.[Name] ProviderName,
O.[PlanType]
FROM FROM
[dbo].[ProviderUser] PU [dbo].[ProviderUser] PU
INNER JOIN INNER JOIN

View File

@ -0,0 +1,358 @@
using System.Security.Claims;
using System.Text.Json;
using AutoFixture;
using Bit.Api.Controllers;
using Bit.Api.Models.Response;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Enums.Provider;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Core.Models.Data;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
namespace Bit.Api.Test.Controllers;
[ControllerCustomize(typeof(SyncController))]
[SutProviderCustomize]
public class SyncControllerTests
{
[Theory]
[BitAutoData]
public async Task Get_ThrowBadRequest_WhenUserNotFound(SutProvider<SyncController> sutProvider)
{
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
async Task<SyncResponseModel> GetAction()
{
return await sutProvider.Sut.Get();
}
await Assert.ThrowsAsync<BadRequestException>((Func<Task<SyncResponseModel>>)GetAction);
}
[Theory]
[BitAutoData]
public async Task Get_Success_AtLeastOneEnabledOrg(User user,
List<List<string>> userEquivalentDomains,
List<GlobalEquivalentDomainsType> userExcludedGlobalEquivalentDomains,
ICollection<OrganizationUserOrganizationDetails> organizationUserDetails,
ICollection<ProviderUserProviderDetails> providerUserDetails,
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
ICollection<Folder> folders,
ICollection<CipherDetails> ciphers,
ICollection<Send> sends,
ICollection<Policy> policies,
ICollection<CollectionDetails> collections,
SutProvider<SyncController> sutProvider)
{
// Get dependencies
var userService = sutProvider.GetDependency<IUserService>();
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
var folderRepository = sutProvider.GetDependency<IFolderRepository>();
var cipherRepository = sutProvider.GetDependency<ICipherRepository>();
var sendRepository = sutProvider.GetDependency<ISendRepository>();
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
// Adjust random data to match required formats / test intentions
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
user.ExcludedGlobalEquivalentDomains = JsonSerializer.Serialize(userExcludedGlobalEquivalentDomains);
// At least 1 org needs to be enabled to fully test
if (!organizationUserDetails.Any(o => o.Enabled))
{
// We need at least 1 enabled org
if (organizationUserDetails.Count > 0)
{
organizationUserDetails.First().Enabled = true;
}
else
{
// create an enabled org
var enabledOrg = new Fixture().Create<OrganizationUserOrganizationDetails>();
enabledOrg.Enabled = true;
organizationUserDetails.Add((enabledOrg));
}
}
// Setup returns
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
organizationUserRepository
.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);
providerUserRepository
.GetManyDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed).Returns(providerUserDetails);
providerUserRepository
.GetManyOrganizationDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed)
.Returns(providerUserOrganizationDetails);
folderRepository.GetManyByUserIdAsync(user.Id).Returns(folders);
cipherRepository.GetManyByUserIdAsync(user.Id).Returns(ciphers);
sendRepository
.GetManyByUserIdAsync(user.Id).Returns(sends);
policyRepository.GetManyByUserIdAsync(user.Id).Returns(policies);
// Returns for methods only called if we have enabled orgs
collectionRepository.GetManyByUserIdAsync(user.Id).Returns(collections);
collectionCipherRepository.GetManyByUserIdAsync(user.Id).Returns(new List<CollectionCipher>());
// Back to standard test setup
userService.TwoFactorIsEnabledAsync(user).Returns(false);
userService.HasPremiumFromOrganization(user).Returns(false);
// Execute GET
var result = await sutProvider.Sut.Get();
// Asserts
// Assert that methods are called
var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);
this.AssertMethodsCalledAsync(userService, organizationUserRepository, providerUserRepository, folderRepository,
cipherRepository, sendRepository, collectionRepository, collectionCipherRepository, hasEnabledOrgs);
Assert.IsType<SyncResponseModel>(result);
// Collections should not be empty when at least 1 org is enabled
Assert.NotEmpty(result.Collections);
}
[Theory]
[BitAutoData]
public async Task Get_Success_AllDisabledOrgs(User user,
List<List<string>> userEquivalentDomains,
List<GlobalEquivalentDomainsType> userExcludedGlobalEquivalentDomains,
ICollection<OrganizationUserOrganizationDetails> organizationUserDetails,
ICollection<ProviderUserProviderDetails> providerUserDetails,
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
ICollection<Folder> folders,
ICollection<CipherDetails> ciphers,
ICollection<Send> sends,
ICollection<Policy> policies,
SutProvider<SyncController> sutProvider)
{
// Get dependencies
var userService = sutProvider.GetDependency<IUserService>();
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
var folderRepository = sutProvider.GetDependency<IFolderRepository>();
var cipherRepository = sutProvider.GetDependency<ICipherRepository>();
var sendRepository = sutProvider.GetDependency<ISendRepository>();
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
// Adjust random data to match required formats / test intentions
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
user.ExcludedGlobalEquivalentDomains = JsonSerializer.Serialize(userExcludedGlobalEquivalentDomains);
// All orgs disabled
if (organizationUserDetails.Count > 0)
{
foreach (var orgUserDetails in organizationUserDetails)
{
orgUserDetails.Enabled = false;
}
}
else
{
var disabledOrg = new Fixture().Create<OrganizationUserOrganizationDetails>();
disabledOrg.Enabled = false;
organizationUserDetails.Add((disabledOrg));
}
// Setup returns
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
organizationUserRepository
.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);
providerUserRepository
.GetManyDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed).Returns(providerUserDetails);
providerUserRepository
.GetManyOrganizationDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed)
.Returns(providerUserOrganizationDetails);
folderRepository.GetManyByUserIdAsync(user.Id).Returns(folders);
cipherRepository.GetManyByUserIdAsync(user.Id).Returns(ciphers);
sendRepository
.GetManyByUserIdAsync(user.Id).Returns(sends);
policyRepository.GetManyByUserIdAsync(user.Id).Returns(policies);
userService.TwoFactorIsEnabledAsync(user).Returns(false);
userService.HasPremiumFromOrganization(user).Returns(false);
// Execute GET
var result = await sutProvider.Sut.Get();
// Asserts
// Assert that methods are called
var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);
this.AssertMethodsCalledAsync(userService, organizationUserRepository, providerUserRepository, folderRepository,
cipherRepository, sendRepository, collectionRepository, collectionCipherRepository, hasEnabledOrgs);
Assert.IsType<SyncResponseModel>(result);
// Collections should be empty when all standard orgs are disabled.
Assert.Empty(result.Collections);
}
// Test where provider org has specific plan type and assert plan type comes out on SyncResponseModel class on ProfileResponseModel
[Theory]
[BitAutoData]
public async Task Get_ProviderPlanTypeProperlyPopulated(User user,
List<List<string>> userEquivalentDomains,
List<GlobalEquivalentDomainsType> userExcludedGlobalEquivalentDomains,
ICollection<OrganizationUserOrganizationDetails> organizationUserDetails,
ICollection<ProviderUserProviderDetails> providerUserDetails,
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
ICollection<Folder> folders,
ICollection<CipherDetails> ciphers,
ICollection<Send> sends,
ICollection<Policy> policies,
ICollection<CollectionDetails> collections,
SutProvider<SyncController> sutProvider)
{
// Get dependencies
var userService = sutProvider.GetDependency<IUserService>();
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
var folderRepository = sutProvider.GetDependency<IFolderRepository>();
var cipherRepository = sutProvider.GetDependency<ICipherRepository>();
var sendRepository = sutProvider.GetDependency<ISendRepository>();
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
// Adjust random data to match required formats / test intentions
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
user.ExcludedGlobalEquivalentDomains = JsonSerializer.Serialize(userExcludedGlobalEquivalentDomains);
// Setup returns
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
organizationUserRepository
.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);
providerUserRepository
.GetManyDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed).Returns(providerUserDetails);
providerUserRepository
.GetManyOrganizationDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed)
.Returns(providerUserOrganizationDetails);
folderRepository.GetManyByUserIdAsync(user.Id).Returns(folders);
cipherRepository.GetManyByUserIdAsync(user.Id).Returns(ciphers);
sendRepository
.GetManyByUserIdAsync(user.Id).Returns(sends);
policyRepository.GetManyByUserIdAsync(user.Id).Returns(policies);
// Returns for methods only called if we have enabled orgs
collectionRepository.GetManyByUserIdAsync(user.Id).Returns(collections);
collectionCipherRepository.GetManyByUserIdAsync(user.Id).Returns(new List<CollectionCipher>());
// Back to standard test setup
userService.TwoFactorIsEnabledAsync(user).Returns(false);
userService.HasPremiumFromOrganization(user).Returns(false);
// Execute GET
var result = await sutProvider.Sut.Get();
// Asserts
// Assert that methods are called
var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);
this.AssertMethodsCalledAsync(userService, organizationUserRepository, providerUserRepository, folderRepository,
cipherRepository, sendRepository, collectionRepository, collectionCipherRepository, hasEnabledOrgs);
Assert.IsType<SyncResponseModel>(result);
// Look up ProviderOrg output and compare to ProviderOrg method inputs to ensure
// product type is set correctly.
foreach (var profProviderOrg in result.Profile.ProviderOrganizations)
{
var matchedProviderUserOrgDetails =
providerUserOrganizationDetails.FirstOrDefault(p => p.OrganizationId.ToString() == profProviderOrg.Id);
if (matchedProviderUserOrgDetails != null)
{
var providerOrgProductType = StaticStore.GetPlan(matchedProviderUserOrgDetails.PlanType).Product;
Assert.Equal(providerOrgProductType, profProviderOrg.PlanProductType);
}
}
}
private async void AssertMethodsCalledAsync(IUserService userService,
IOrganizationUserRepository organizationUserRepository,
IProviderUserRepository providerUserRepository, IFolderRepository folderRepository,
ICipherRepository cipherRepository, ISendRepository sendRepository,
ICollectionRepository collectionRepository,
ICollectionCipherRepository collectionCipherRepository,
bool hasEnabledOrgs)
{
await userService.ReceivedWithAnyArgs(1).GetUserByPrincipalAsync(default);
await organizationUserRepository.ReceivedWithAnyArgs(1)
.GetManyDetailsByUserAsync(default);
await providerUserRepository.ReceivedWithAnyArgs(1)
.GetManyDetailsByUserAsync(default);
await providerUserRepository.ReceivedWithAnyArgs(1)
.GetManyOrganizationDetailsByUserAsync(default);
await folderRepository.ReceivedWithAnyArgs(1)
.GetManyByUserIdAsync(default);
await cipherRepository.ReceivedWithAnyArgs(1)
.GetManyByUserIdAsync(default);
await sendRepository.ReceivedWithAnyArgs(1)
.GetManyByUserIdAsync(default);
// These two are only called when at least 1 enabled org.
if (hasEnabledOrgs)
{
await collectionRepository.ReceivedWithAnyArgs(1)
.GetManyByUserIdAsync(default);
await collectionCipherRepository.ReceivedWithAnyArgs(1)
.GetManyByUserIdAsync(default);
}
else
{
// all disabled orgs
await collectionRepository.ReceivedWithAnyArgs(0)
.GetManyByUserIdAsync(default);
await collectionCipherRepository.ReceivedWithAnyArgs(0)
.GetManyByUserIdAsync(default);
}
await userService.ReceivedWithAnyArgs(1)
.TwoFactorIsEnabledAsync(default);
await userService.ReceivedWithAnyArgs(1)
.HasPremiumFromOrganization(default);
}
}

View File

@ -0,0 +1,71 @@
-- SG-992 changes: add planType to provider orgs
CREATE OR ALTER VIEW [dbo].[ProviderUserProviderOrganizationDetailsView]
AS
SELECT
PU.[UserId],
PO.[OrganizationId],
O.[Name],
O.[Enabled],
O.[UsePolicies],
O.[UseSso],
O.[UseKeyConnector],
O.[UseScim],
O.[UseGroups],
O.[UseDirectory],
O.[UseEvents],
O.[UseTotp],
O.[Use2fa],
O.[UseApi],
O.[UseResetPassword],
O.[SelfHost],
O.[UsersGetPremium],
O.[UseCustomPermissions],
O.[Seats],
O.[MaxCollections],
O.[MaxStorageGb],
O.[Identifier],
PO.[Key],
O.[PublicKey],
O.[PrivateKey],
PU.[Status],
PU.[Type],
PO.[ProviderId],
PU.[Id] ProviderUserId,
P.[Name] ProviderName,
O.[PlanType] -- new prop
FROM
[dbo].[ProviderUser] PU
INNER JOIN
[dbo].[ProviderOrganization] PO ON PO.[ProviderId] = PU.[ProviderId]
INNER JOIN
[dbo].[Organization] O ON O.[Id] = PO.[OrganizationId]
INNER JOIN
[dbo].[Provider] P ON P.[Id] = PU.[ProviderId]
GO
-- Refresh metadata of stored procs & functions that use the updated view
IF OBJECT_ID('[dbo].[ProviderUserProviderOrganizationDetails_ReadByUserIdStatus]') IS NOT NULL
BEGIN
EXECUTE sp_refreshsqlmodule N'[dbo].[ProviderUserProviderOrganizationDetails_ReadByUserIdStatus]';
END
GO
-- EC-591 / SG-996 changes: add optional status to stored proc
CREATE OR ALTER PROCEDURE [dbo].[ProviderUserUserDetails_ReadByProviderId]
@ProviderId UNIQUEIDENTIFIER,
@Status TINYINT = NULL -- new: this is required to be backwards compatible
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[ProviderUserUserDetailsView]
WHERE
[ProviderId] = @ProviderId
AND [Status] = COALESCE(@Status, [Status]) -- new
END
GO