mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
[PM-23134] Update PolicyDetails sprocs for performance (#6421)
* Add integration tests for GetByUserIdWithPolicyDetailsAsync in OrganizationUserRepository - Implemented multiple test cases to verify the behavior of GetByUserIdWithPolicyDetailsAsync for different user statuses (Confirmed, Accepted, Invited, Revoked). - Ensured that the method returns correct policy details based on user status and organization. - Added tests for scenarios with multiple organizations and non-existing policy types. - Included checks for provider users and custom user permissions. These tests enhance coverage and ensure the correctness of policy retrieval logic. * Add UserProviderAccessView to identify which organizations a user can access as a provider * Refactor PolicyDetails_ReadByUserId stored procedure to improve user access logic - Introduced a Common Table Expression (CTE) for organization users to streamline the selection process based on user status and email. - Added a CTE for providers to enhance clarity and maintainability. - Updated the main query to utilize the new CTEs, improving readability and performance. - Ensured that the procedure correctly identifies provider access based on user permissions. * Refactor OrganizationUser_ReadByUserIdWithPolicyDetails stored procedure to enhance user access logic - Introduced a Common Table Expression (CTE) for organization users to improve selection based on user status and email. - Updated the main query to utilize the new CTEs, enhancing readability and performance. - Adjusted the logic for identifying provider access to ensure accurate policy retrieval based on user permissions. * Add new SQL migration script to refactor policy details queries - Created a new view, UserProviderAccessView, to streamline user access to provider organizations. - Introduced two stored procedures: PolicyDetails_ReadByUserId and OrganizationUser_ReadByUserIdWithPolicyDetails, enhancing the logic for retrieving policy details based on user ID and policy type. - Utilized Common Table Expressions (CTEs) to improve query readability and performance, ensuring accurate policy retrieval based on user permissions and organization status. * Remove GetPolicyDetailsByUserIdTests * Refactor PolicyRequirementQuery to use GetPolicyDetailsByUserIdsAndPolicyType and update unit tests * Remove GetPolicyDetailsByUserId method from IPolicyRepository and its implementations in PolicyRepository classes * Revert changes to PolicyDetails_ReadByUserId stored procedure * Refactor OrganizationUser_ReadByUserIdWithPolicyDetails stored procedure to use UNION instead of OR * Reduce UserEmail variable size from NVARCHAR(320) to NVARCHAR(256) for consistency in stored procedures * Bump date on migration script
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
@@ -20,7 +18,7 @@ public class PolicyRequirementQuery(
|
||||
throw new NotImplementedException("No Requirement Factory found for " + typeof(T));
|
||||
}
|
||||
|
||||
var policyDetails = await GetPolicyDetails(userId);
|
||||
var policyDetails = await GetPolicyDetails(userId, factory.PolicyType);
|
||||
var filteredPolicies = policyDetails
|
||||
.Where(p => p.PolicyType == factory.PolicyType)
|
||||
.Where(factory.Enforce);
|
||||
@@ -48,8 +46,8 @@ public class PolicyRequirementQuery(
|
||||
return eligibleOrganizationUserIds;
|
||||
}
|
||||
|
||||
private Task<IEnumerable<PolicyDetails>> GetPolicyDetails(Guid userId)
|
||||
=> policyRepository.GetPolicyDetailsByUserId(userId);
|
||||
private async Task<IEnumerable<OrganizationPolicyDetails>> GetPolicyDetails(Guid userId, PolicyType policyType)
|
||||
=> await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType([userId], policyType);
|
||||
|
||||
private async Task<IEnumerable<OrganizationPolicyDetails>> GetOrganizationPolicyDetails(Guid organizationId, PolicyType policyType)
|
||||
=> await policyRepository.GetPolicyDetailsByOrganizationIdAsync(organizationId, policyType);
|
||||
|
||||
@@ -20,17 +20,6 @@ public interface IPolicyRepository : IRepository<Policy, Guid>
|
||||
Task<Policy?> GetByOrganizationIdTypeAsync(Guid organizationId, PolicyType type);
|
||||
Task<ICollection<Policy>> GetManyByOrganizationIdAsync(Guid organizationId);
|
||||
Task<ICollection<Policy>> GetManyByUserIdAsync(Guid userId);
|
||||
/// <summary>
|
||||
/// Gets all PolicyDetails for a user for all policy types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Each PolicyDetail represents an OrganizationUser and a Policy which *may* be enforced
|
||||
/// against them. It only returns PolicyDetails for policies that are enabled and where the organization's plan
|
||||
/// supports policies. It also excludes "revoked invited" users who are not subject to policy enforcement.
|
||||
/// This is consumed by <see cref="IPolicyRequirementQuery"/> to create requirements for specific policy types.
|
||||
/// You probably do not want to call it directly.
|
||||
/// </remarks>
|
||||
Task<IEnumerable<PolicyDetails>> GetPolicyDetailsByUserId(Guid userId);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves <see cref="OrganizationPolicyDetails"/> of the specified <paramref name="policyType"/>
|
||||
|
||||
@@ -61,19 +61,6 @@ public class PolicyRepository : Repository<Policy, Guid>, IPolicyRepository
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<PolicyDetails>> GetPolicyDetailsByUserId(Guid userId)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<PolicyDetails>(
|
||||
$"[{Schema}].[PolicyDetails_ReadByUserId]",
|
||||
new { UserId = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<OrganizationPolicyDetails>> GetPolicyDetailsByUserIdsAndPolicyType(IEnumerable<Guid> userIds, PolicyType type)
|
||||
{
|
||||
await using var connection = new SqlConnection(ConnectionString);
|
||||
|
||||
@@ -56,45 +56,6 @@ public class PolicyRepository : Repository<AdminConsoleEntities.Policy, Policy,
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<PolicyDetails>> GetPolicyDetailsByUserId(Guid userId)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var providerOrganizations = from pu in dbContext.ProviderUsers
|
||||
where pu.UserId == userId
|
||||
join po in dbContext.ProviderOrganizations
|
||||
on pu.ProviderId equals po.ProviderId
|
||||
select po;
|
||||
|
||||
var query = from p in dbContext.Policies
|
||||
join ou in dbContext.OrganizationUsers
|
||||
on p.OrganizationId equals ou.OrganizationId
|
||||
join o in dbContext.Organizations
|
||||
on p.OrganizationId equals o.Id
|
||||
where
|
||||
p.Enabled &&
|
||||
o.Enabled &&
|
||||
o.UsePolicies &&
|
||||
(
|
||||
(ou.Status != OrganizationUserStatusType.Invited && ou.UserId == userId) ||
|
||||
// Invited orgUsers do not have a UserId associated with them, so we have to match up their email
|
||||
(ou.Status == OrganizationUserStatusType.Invited && ou.Email == dbContext.Users.Find(userId).Email)
|
||||
)
|
||||
select new PolicyDetails
|
||||
{
|
||||
OrganizationUserId = ou.Id,
|
||||
OrganizationId = p.OrganizationId,
|
||||
PolicyType = p.Type,
|
||||
PolicyData = p.Data,
|
||||
OrganizationUserType = ou.Type,
|
||||
OrganizationUserStatus = ou.Status,
|
||||
OrganizationUserPermissionsData = ou.Permissions,
|
||||
IsProvider = providerOrganizations.Any(po => po.OrganizationId == p.OrganizationId)
|
||||
};
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<OrganizationPolicyDetails>> GetPolicyDetailsByOrganizationIdAsync(Guid organizationId, PolicyType policyType)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
|
||||
@@ -4,31 +4,70 @@
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
SELECT
|
||||
OU.[Id] AS OrganizationUserId,
|
||||
P.[OrganizationId],
|
||||
P.[Type] AS PolicyType,
|
||||
P.[Enabled] AS PolicyEnabled,
|
||||
P.[Data] AS PolicyData,
|
||||
OU.[Type] AS OrganizationUserType,
|
||||
OU.[Status] AS OrganizationUserStatus,
|
||||
OU.[Permissions] AS OrganizationUserPermissionsData,
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM [dbo].[ProviderUserView] PU
|
||||
INNER JOIN [dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId]
|
||||
WHERE PU.[UserId] = OU.[UserId] AND PO.[OrganizationId] = P.[OrganizationId]
|
||||
) THEN 1 ELSE 0 END AS IsProvider
|
||||
FROM [dbo].[PolicyView] P
|
||||
INNER JOIN [dbo].[OrganizationUserView] OU
|
||||
ON P.[OrganizationId] = OU.[OrganizationId]
|
||||
WHERE P.[Type] = @PolicyType AND
|
||||
|
||||
DECLARE @UserEmail NVARCHAR(256)
|
||||
SELECT @UserEmail = Email
|
||||
FROM
|
||||
[dbo].[UserView]
|
||||
WHERE
|
||||
Id = @UserId
|
||||
|
||||
;WITH OrgUsers AS
|
||||
(
|
||||
(OU.[Status] != 0 AND OU.[UserId] = @UserId) -- OrgUsers who have accepted their invite and are linked to a UserId
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM [dbo].[UserView] U
|
||||
WHERE U.[Id] = @UserId AND OU.[Email] = U.[Email] AND OU.[Status] = 0 -- 'Invited' OrgUsers are not linked to a UserId yet, so we have to look up their email
|
||||
)
|
||||
-- All users except invited (Status <> 0): direct UserId match
|
||||
SELECT
|
||||
OU.[Id],
|
||||
OU.[OrganizationId],
|
||||
OU.[Type],
|
||||
OU.[Status],
|
||||
OU.[Permissions]
|
||||
FROM
|
||||
[dbo].[OrganizationUserView] OU
|
||||
WHERE
|
||||
OU.[Status] <> 0
|
||||
AND OU.[UserId] = @UserId
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Invited users: email match
|
||||
SELECT
|
||||
OU.[Id],
|
||||
OU.[OrganizationId],
|
||||
OU.[Type],
|
||||
OU.[Status],
|
||||
OU.[Permissions]
|
||||
FROM
|
||||
[dbo].[OrganizationUserView] OU
|
||||
WHERE
|
||||
OU.[Status] = 0
|
||||
AND OU.[Email] = @UserEmail
|
||||
AND @UserEmail IS NOT NULL
|
||||
),
|
||||
Providers AS
|
||||
(
|
||||
SELECT
|
||||
OrganizationId
|
||||
FROM
|
||||
[dbo].[UserProviderAccessView]
|
||||
WHERE
|
||||
UserId = @UserId
|
||||
)
|
||||
END
|
||||
SELECT
|
||||
OU.[Id] AS [OrganizationUserId],
|
||||
P.[OrganizationId],
|
||||
P.[Type] AS [PolicyType],
|
||||
P.[Enabled] AS [PolicyEnabled],
|
||||
P.[Data] AS [PolicyData],
|
||||
OU.[Type] AS [OrganizationUserType],
|
||||
OU.[Status] AS [OrganizationUserStatus],
|
||||
OU.[Permissions] AS [OrganizationUserPermissionsData],
|
||||
CASE WHEN PR.[OrganizationId] IS NULL THEN 0 ELSE 1 END AS [IsProvider]
|
||||
FROM
|
||||
[dbo].[PolicyView] P
|
||||
INNER JOIN
|
||||
OrgUsers OU ON P.[OrganizationId] = OU.[OrganizationId]
|
||||
LEFT JOIN
|
||||
Providers PR ON PR.[OrganizationId] = OU.[OrganizationId]
|
||||
WHERE
|
||||
P.[Type] = @PolicyType
|
||||
END
|
||||
|
||||
9
src/Sql/dbo/Views/UserProviderAccessView.sql
Normal file
9
src/Sql/dbo/Views/UserProviderAccessView.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
CREATE VIEW [dbo].[UserProviderAccessView]
|
||||
AS
|
||||
SELECT DISTINCT
|
||||
PU.[UserId],
|
||||
PO.[OrganizationId]
|
||||
FROM
|
||||
[dbo].[ProviderUserView] PU
|
||||
INNER JOIN
|
||||
[dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId]
|
||||
@@ -14,10 +14,12 @@ public class PolicyRequirementQueryTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetAsync_IgnoresOtherPolicyTypes(Guid userId)
|
||||
{
|
||||
var thisPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg };
|
||||
var otherPolicy = new PolicyDetails { PolicyType = PolicyType.RequireSso };
|
||||
var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = userId };
|
||||
var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.RequireSso, UserId = userId };
|
||||
var policyRepository = Substitute.For<IPolicyRepository>();
|
||||
policyRepository.GetPolicyDetailsByUserId(userId).Returns([otherPolicy, thisPolicy]);
|
||||
policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(userId)), PolicyType.SingleOrg)
|
||||
.Returns([otherPolicy, thisPolicy]);
|
||||
|
||||
var factory = new TestPolicyRequirementFactory(_ => true);
|
||||
var sut = new PolicyRequirementQuery(policyRepository, [factory]);
|
||||
@@ -33,9 +35,11 @@ public class PolicyRequirementQueryTests
|
||||
{
|
||||
// Arrange policies
|
||||
var policyRepository = Substitute.For<IPolicyRepository>();
|
||||
var thisPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg };
|
||||
var otherPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg };
|
||||
policyRepository.GetPolicyDetailsByUserId(userId).Returns([thisPolicy, otherPolicy]);
|
||||
var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = userId };
|
||||
var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = userId };
|
||||
policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(userId)), PolicyType.SingleOrg)
|
||||
.Returns([thisPolicy, otherPolicy]);
|
||||
|
||||
// Arrange a substitute Enforce function so that we can inspect the received calls
|
||||
var callback = Substitute.For<Func<PolicyDetails, bool>>();
|
||||
@@ -70,7 +74,9 @@ public class PolicyRequirementQueryTests
|
||||
public async Task GetAsync_HandlesNoPolicies(Guid userId)
|
||||
{
|
||||
var policyRepository = Substitute.For<IPolicyRepository>();
|
||||
policyRepository.GetPolicyDetailsByUserId(userId).Returns([]);
|
||||
policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(userId)), PolicyType.SingleOrg)
|
||||
.Returns([]);
|
||||
|
||||
var factory = new TestPolicyRequirementFactory(x => x.IsProvider);
|
||||
var sut = new PolicyRequirementQuery(policyRepository, [factory]);
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.OrganizationUserRepository;
|
||||
|
||||
public class GetByUserIdWithPolicyDetailsTests
|
||||
{
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetByUserIdWithPolicyDetailsAsync_WithConfirmedUser_ReturnsPolicy(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var org = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Org",
|
||||
BillingEmail = "billing@example.com",
|
||||
Plan = "Test",
|
||||
});
|
||||
var orgUser = new OrganizationUser
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.User,
|
||||
Email = null
|
||||
};
|
||||
await organizationUserRepository.CreateAsync(orgUser);
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
Enabled = true,
|
||||
Type = PolicyType.SingleOrg,
|
||||
Data = CoreHelpers.ClassToJsonData(new { Setting = "value" })
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg);
|
||||
|
||||
// Assert
|
||||
var policyDetails = result.Single();
|
||||
Assert.Equal(orgUser.Id, policyDetails.OrganizationUserId);
|
||||
Assert.Equal(org.Id, policyDetails.OrganizationId);
|
||||
Assert.Equal(PolicyType.SingleOrg, policyDetails.PolicyType);
|
||||
Assert.True(policyDetails.PolicyEnabled);
|
||||
Assert.Equal(OrganizationUserType.User, policyDetails.OrganizationUserType);
|
||||
Assert.Equal(OrganizationUserStatusType.Confirmed, policyDetails.OrganizationUserStatus);
|
||||
Assert.False(policyDetails.IsProvider);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetByUserIdWithPolicyDetailsAsync_WithAcceptedUser_ReturnsPolicy(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var org = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Org",
|
||||
BillingEmail = "billing@example.com",
|
||||
Plan = "Test",
|
||||
});
|
||||
var orgUser = new OrganizationUser
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Accepted,
|
||||
Type = OrganizationUserType.Admin,
|
||||
Email = null
|
||||
};
|
||||
await organizationUserRepository.CreateAsync(orgUser);
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
Enabled = false, // Note: disabled policy
|
||||
Type = PolicyType.RequireSso,
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.RequireSso);
|
||||
|
||||
// Assert
|
||||
var policyDetails = result.Single();
|
||||
Assert.Equal(orgUser.Id, policyDetails.OrganizationUserId);
|
||||
Assert.False(policyDetails.PolicyEnabled); // Should return even if disabled
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetByUserIdWithPolicyDetailsAsync_WithInvitedUser_ReturnsPolicy(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var org = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Org",
|
||||
BillingEmail = "billing@example.com",
|
||||
Plan = "Test",
|
||||
});
|
||||
var orgUser = new OrganizationUser
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
UserId = null, // invited users have null userId
|
||||
Status = OrganizationUserStatusType.Invited,
|
||||
Type = OrganizationUserType.User,
|
||||
Email = user.Email // invited users have matching Email
|
||||
};
|
||||
await organizationUserRepository.CreateAsync(orgUser);
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
Enabled = true,
|
||||
Type = PolicyType.TwoFactorAuthentication,
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.TwoFactorAuthentication);
|
||||
|
||||
// Assert
|
||||
var policyDetails = result.Single();
|
||||
Assert.Equal(orgUser.Id, policyDetails.OrganizationUserId);
|
||||
Assert.Equal(OrganizationUserStatusType.Invited, policyDetails.OrganizationUserStatus);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetByUserIdWithPolicyDetailsAsync_WithRevokedUser_ReturnsPolicy(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var org = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Org",
|
||||
BillingEmail = "billing@example.com",
|
||||
Plan = "Test",
|
||||
});
|
||||
var orgUser = new OrganizationUser
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Revoked,
|
||||
Type = OrganizationUserType.Owner,
|
||||
Email = null
|
||||
};
|
||||
await organizationUserRepository.CreateAsync(orgUser);
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
Enabled = true,
|
||||
Type = PolicyType.SingleOrg,
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg);
|
||||
|
||||
// Assert
|
||||
var policyDetails = result.Single();
|
||||
Assert.Equal(OrganizationUserStatusType.Revoked, policyDetails.OrganizationUserStatus);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetByUserIdWithPolicyDetailsAsync_WithMultipleOrganizations_ReturnsAllMatchingPolicies(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
|
||||
// Org1 with SingleOrg policy
|
||||
var org1 = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Org 1",
|
||||
BillingEmail = "billing@example.com",
|
||||
Plan = "Test",
|
||||
});
|
||||
var orgUser1 = new OrganizationUser
|
||||
{
|
||||
OrganizationId = org1.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.User,
|
||||
};
|
||||
await organizationUserRepository.CreateAsync(orgUser1);
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org1.Id,
|
||||
Enabled = true,
|
||||
Type = PolicyType.SingleOrg,
|
||||
});
|
||||
|
||||
// Org2 with SingleOrg policy
|
||||
var org2 = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Org 2",
|
||||
BillingEmail = "billing2@example.com",
|
||||
Plan = "Test",
|
||||
});
|
||||
var orgUser2 = new OrganizationUser
|
||||
{
|
||||
OrganizationId = org2.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.Admin,
|
||||
};
|
||||
await organizationUserRepository.CreateAsync(orgUser2);
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org2.Id,
|
||||
Enabled = true,
|
||||
Type = PolicyType.SingleOrg,
|
||||
});
|
||||
|
||||
// Org3 with RequireSso policy (different type - should not be returned)
|
||||
var org3 = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Org 3",
|
||||
BillingEmail = "billing3@example.com",
|
||||
Plan = "Test",
|
||||
});
|
||||
var orgUser3 = new OrganizationUser
|
||||
{
|
||||
OrganizationId = org3.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.Owner,
|
||||
};
|
||||
await organizationUserRepository.CreateAsync(orgUser3);
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org3.Id,
|
||||
Enabled = true,
|
||||
Type = PolicyType.RequireSso,
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = (await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg)).ToList();
|
||||
|
||||
// Assert - should only get 2 policies (org1 and org2)
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, p => p.OrganizationId == org1.Id && p.OrganizationUserType == OrganizationUserType.User);
|
||||
Assert.Contains(result, p => p.OrganizationId == org2.Id && p.OrganizationUserType == OrganizationUserType.Admin);
|
||||
Assert.DoesNotContain(result, p => p.OrganizationId == org3.Id);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetByUserIdWithPolicyDetailsAsync_WithNonExistingPolicyType_ReturnsEmpty(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var org = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Org",
|
||||
BillingEmail = "billing@example.com",
|
||||
Plan = "Test",
|
||||
});
|
||||
await organizationUserRepository.CreateTestOrganizationUserAsync(org, user);
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
Enabled = true,
|
||||
Type = PolicyType.SingleOrg,
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.RequireSso);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetByUserIdWithPolicyDetailsAsync_WithProviderUser_ReturnsIsProviderTrue(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository,
|
||||
IProviderRepository providerRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var org = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Org",
|
||||
BillingEmail = "billing@example.com",
|
||||
Plan = "Test",
|
||||
});
|
||||
var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user);
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
Enabled = true,
|
||||
Type = PolicyType.SingleOrg,
|
||||
});
|
||||
var provider = await providerRepository.CreateAsync(new Provider
|
||||
{
|
||||
Name = Guid.NewGuid().ToString(),
|
||||
Enabled = true
|
||||
});
|
||||
await providerUserRepository.CreateAsync(new ProviderUser
|
||||
{
|
||||
ProviderId = provider.Id,
|
||||
UserId = user.Id,
|
||||
Status = ProviderUserStatusType.Confirmed
|
||||
});
|
||||
await providerOrganizationRepository.CreateAsync(new ProviderOrganization
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
ProviderId = provider.Id
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg);
|
||||
|
||||
// Assert
|
||||
var policyDetails = result.Single();
|
||||
Assert.True(policyDetails.IsProvider);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetByUserIdWithPolicyDetailsAsync_WithCustomUserWithPermissions_ReturnsPermissions(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var org = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Org",
|
||||
BillingEmail = "billing@example.com",
|
||||
Plan = "Test",
|
||||
});
|
||||
var orgUser = new OrganizationUser
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.Custom,
|
||||
Email = null
|
||||
};
|
||||
orgUser.SetPermissions(new Permissions
|
||||
{
|
||||
ManagePolicies = true,
|
||||
EditAnyCollection = true
|
||||
});
|
||||
await organizationUserRepository.CreateAsync(orgUser);
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
Enabled = true,
|
||||
Type = PolicyType.SingleOrg,
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg);
|
||||
|
||||
// Assert
|
||||
var policyDetails = result.Single();
|
||||
Assert.NotNull(policyDetails.OrganizationUserPermissionsData);
|
||||
var permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(policyDetails.OrganizationUserPermissionsData);
|
||||
Assert.True(permissions.ManagePolicies);
|
||||
Assert.True(permissions.EditAnyCollection);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetByUserIdWithPolicyDetailsAsync_WhenNoPolicyExists_ReturnsEmpty(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var org = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Org",
|
||||
BillingEmail = "billing@example.com",
|
||||
Plan = "Test",
|
||||
});
|
||||
await organizationUserRepository.CreateTestOrganizationUserAsync(org, user);
|
||||
|
||||
// Act
|
||||
var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetByUserIdWithPolicyDetailsAsync_WhenUserNotInOrg_ReturnsEmpty(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var org = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Org",
|
||||
BillingEmail = "billing@example.com",
|
||||
Plan = "Test",
|
||||
});
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
Enabled = true,
|
||||
Type = PolicyType.SingleOrg,
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,385 +0,0 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.PolicyRepository;
|
||||
|
||||
public class GetPolicyDetailsByUserIdTests
|
||||
{
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserId_NonInvitedUsers_Works(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
// OrgUser1 - owner of org1 - confirmed
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var org1 = await CreateEnterpriseOrg(organizationRepository);
|
||||
var orgUser1 = new OrganizationUser
|
||||
{
|
||||
OrganizationId = org1.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.Owner,
|
||||
Email = null // confirmed OrgUsers use the email on the User table
|
||||
};
|
||||
await organizationUserRepository.CreateAsync(orgUser1);
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org1.Id,
|
||||
Enabled = true,
|
||||
Type = PolicyType.SingleOrg,
|
||||
Data = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = true, IntSetting = 5 })
|
||||
});
|
||||
|
||||
// OrgUser2 - custom user of org2 - accepted
|
||||
var org2 = await CreateEnterpriseOrg(organizationRepository);
|
||||
var orgUser2 = new OrganizationUser
|
||||
{
|
||||
OrganizationId = org2.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Accepted,
|
||||
Type = OrganizationUserType.Custom,
|
||||
Email = null // accepted OrgUsers use the email on the User table
|
||||
};
|
||||
orgUser2.SetPermissions(new Permissions
|
||||
{
|
||||
ManagePolicies = true
|
||||
});
|
||||
await organizationUserRepository.CreateAsync(orgUser2);
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org2.Id,
|
||||
Enabled = true,
|
||||
Type = PolicyType.SingleOrg,
|
||||
Data = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = false, IntSetting = 15 })
|
||||
});
|
||||
|
||||
// Act
|
||||
var policyDetails = (await policyRepository.GetPolicyDetailsByUserId(user.Id)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, policyDetails.Count);
|
||||
|
||||
var actualPolicyDetails1 = policyDetails.Find(p => p.OrganizationUserId == orgUser1.Id);
|
||||
var expectedPolicyDetails1 = new PolicyDetails
|
||||
{
|
||||
OrganizationUserId = orgUser1.Id,
|
||||
OrganizationId = org1.Id,
|
||||
PolicyType = PolicyType.SingleOrg,
|
||||
PolicyData = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = true, IntSetting = 5 }),
|
||||
OrganizationUserType = OrganizationUserType.Owner,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Confirmed,
|
||||
OrganizationUserPermissionsData = null,
|
||||
IsProvider = false
|
||||
};
|
||||
Assert.Equivalent(expectedPolicyDetails1, actualPolicyDetails1);
|
||||
Assert.Equivalent(expectedPolicyDetails1.GetDataModel<TestPolicyData>(), new TestPolicyData { BoolSetting = true, IntSetting = 5 });
|
||||
|
||||
var actualPolicyDetails2 = policyDetails.Find(p => p.OrganizationUserId == orgUser2.Id);
|
||||
var expectedPolicyDetails2 = new PolicyDetails
|
||||
{
|
||||
OrganizationUserId = orgUser2.Id,
|
||||
OrganizationId = org2.Id,
|
||||
PolicyType = PolicyType.SingleOrg,
|
||||
PolicyData = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = false, IntSetting = 15 }),
|
||||
OrganizationUserType = OrganizationUserType.Custom,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Accepted,
|
||||
OrganizationUserPermissionsData = CoreHelpers.ClassToJsonData(new Permissions { ManagePolicies = true }),
|
||||
IsProvider = false
|
||||
};
|
||||
Assert.Equivalent(expectedPolicyDetails2, actualPolicyDetails2);
|
||||
Assert.Equivalent(expectedPolicyDetails2.GetDataModel<TestPolicyData>(), new TestPolicyData { BoolSetting = false, IntSetting = 15 });
|
||||
Assert.Equivalent(new Permissions { ManagePolicies = true }, actualPolicyDetails2.GetOrganizationUserCustomPermissions(), strict: true);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserId_InvitedUser_Works(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var org = await CreateEnterpriseOrg(organizationRepository);
|
||||
var orgUser = new OrganizationUser
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
UserId = null, // invited users have null userId
|
||||
Status = OrganizationUserStatusType.Invited,
|
||||
Type = OrganizationUserType.Custom,
|
||||
Email = user.Email // invited users have matching Email
|
||||
};
|
||||
await organizationUserRepository.CreateAsync(orgUser);
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
Enabled = true,
|
||||
Type = PolicyType.SingleOrg,
|
||||
});
|
||||
|
||||
// Act
|
||||
var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id);
|
||||
|
||||
// Assert
|
||||
var expectedPolicyDetails = new PolicyDetails
|
||||
{
|
||||
OrganizationUserId = orgUser.Id,
|
||||
OrganizationId = org.Id,
|
||||
PolicyType = PolicyType.SingleOrg,
|
||||
OrganizationUserType = OrganizationUserType.Custom,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Invited,
|
||||
IsProvider = false
|
||||
};
|
||||
|
||||
Assert.Equivalent(expectedPolicyDetails, actualPolicyDetails.Single());
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserId_RevokedConfirmedUser_Works(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var org = await CreateEnterpriseOrg(organizationRepository);
|
||||
// User has been confirmed to the org but then revoked
|
||||
var orgUser = new OrganizationUser
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Revoked,
|
||||
Type = OrganizationUserType.Owner,
|
||||
Email = null
|
||||
};
|
||||
await organizationUserRepository.CreateAsync(orgUser);
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
Enabled = true,
|
||||
Type = PolicyType.SingleOrg,
|
||||
});
|
||||
|
||||
// Act
|
||||
var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id);
|
||||
|
||||
// Assert
|
||||
var expectedPolicyDetails = new PolicyDetails
|
||||
{
|
||||
OrganizationUserId = orgUser.Id,
|
||||
OrganizationId = org.Id,
|
||||
PolicyType = PolicyType.SingleOrg,
|
||||
OrganizationUserType = OrganizationUserType.Owner,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Revoked,
|
||||
IsProvider = false
|
||||
};
|
||||
|
||||
Assert.Equivalent(expectedPolicyDetails, actualPolicyDetails.Single());
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserId_RevokedInvitedUser_DoesntReturnPolicies(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var org = await CreateEnterpriseOrg(organizationRepository);
|
||||
// User has been invited to the org but then revoked - without ever being confirmed and linked to a user.
|
||||
// This is an unhandled edge case because those users will go through policy enforcement later,
|
||||
// as part of accepting their invite after being restored. For now this is just documented as expected behavior.
|
||||
var orgUser = new OrganizationUser
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
UserId = null,
|
||||
Status = OrganizationUserStatusType.Revoked,
|
||||
Type = OrganizationUserType.Owner,
|
||||
Email = user.Email
|
||||
};
|
||||
await organizationUserRepository.CreateAsync(orgUser);
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
Enabled = true,
|
||||
Type = PolicyType.SingleOrg,
|
||||
});
|
||||
|
||||
// Act
|
||||
var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id);
|
||||
|
||||
Assert.Empty(actualPolicyDetails);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserId_SetsIsProvider(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository,
|
||||
IProviderRepository providerRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var org = await CreateEnterpriseOrg(organizationRepository);
|
||||
var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user);
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
Enabled = true,
|
||||
Type = PolicyType.SingleOrg,
|
||||
});
|
||||
|
||||
// Arrange provider
|
||||
var provider = await providerRepository.CreateAsync(new Provider
|
||||
{
|
||||
Name = Guid.NewGuid().ToString(),
|
||||
Enabled = true
|
||||
});
|
||||
await providerUserRepository.CreateAsync(new ProviderUser
|
||||
{
|
||||
ProviderId = provider.Id,
|
||||
UserId = user.Id,
|
||||
Status = ProviderUserStatusType.Confirmed
|
||||
});
|
||||
await providerOrganizationRepository.CreateAsync(new ProviderOrganization
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
ProviderId = provider.Id
|
||||
});
|
||||
|
||||
// Act
|
||||
var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id);
|
||||
|
||||
// Assert
|
||||
var expectedPolicyDetails = new PolicyDetails
|
||||
{
|
||||
OrganizationUserId = orgUser.Id,
|
||||
OrganizationId = org.Id,
|
||||
PolicyType = PolicyType.SingleOrg,
|
||||
OrganizationUserType = OrganizationUserType.Owner,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Confirmed,
|
||||
IsProvider = true
|
||||
};
|
||||
|
||||
Assert.Equivalent(expectedPolicyDetails, actualPolicyDetails.Single());
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserId_IgnoresDisabledOrganizations(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var org = await CreateEnterpriseOrg(organizationRepository);
|
||||
await organizationUserRepository.CreateTestOrganizationUserAsync(org, user);
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
Enabled = true,
|
||||
Type = PolicyType.SingleOrg,
|
||||
});
|
||||
|
||||
// Org is disabled; its policies remain, but it is now inactive
|
||||
org.Enabled = false;
|
||||
await organizationRepository.ReplaceAsync(org);
|
||||
|
||||
// Act
|
||||
var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(actualPolicyDetails);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserId_IgnoresDowngradedOrganizations(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var org = await CreateEnterpriseOrg(organizationRepository);
|
||||
await organizationUserRepository.CreateTestOrganizationUserAsync(org, user);
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
Enabled = true,
|
||||
Type = PolicyType.SingleOrg,
|
||||
});
|
||||
|
||||
// Org is downgraded; its policies remain but its plan no longer supports them
|
||||
org.UsePolicies = false;
|
||||
org.PlanType = PlanType.TeamsAnnually;
|
||||
await organizationRepository.ReplaceAsync(org);
|
||||
|
||||
// Act
|
||||
var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(actualPolicyDetails);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task GetPolicyDetailsByUserId_IgnoresDisabledPolicies(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository)
|
||||
{
|
||||
// Arrange
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var org = await CreateEnterpriseOrg(organizationRepository);
|
||||
await organizationUserRepository.CreateTestOrganizationUserAsync(org, user);
|
||||
await policyRepository.CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
Enabled = false,
|
||||
Type = PolicyType.SingleOrg,
|
||||
});
|
||||
|
||||
// Act
|
||||
var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(actualPolicyDetails);
|
||||
}
|
||||
|
||||
private class TestPolicyData : IPolicyDataModel
|
||||
{
|
||||
public bool BoolSetting { get; set; }
|
||||
public int IntSetting { get; set; }
|
||||
}
|
||||
|
||||
private Task<Organization> CreateEnterpriseOrg(IOrganizationRepository organizationRepository)
|
||||
=> organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = Guid.NewGuid().ToString(),
|
||||
BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL
|
||||
Plan = "Test", // TODO: EF does not enforce this being NOT NULl
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
UsePolicies = true
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
CREATE OR ALTER VIEW [dbo].[UserProviderAccessView]
|
||||
AS
|
||||
SELECT DISTINCT
|
||||
PU.[UserId],
|
||||
PO.[OrganizationId]
|
||||
FROM
|
||||
[dbo].[ProviderUserView] PU
|
||||
INNER JOIN
|
||||
[dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId]
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_ReadByUserIdWithPolicyDetails]
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@PolicyType TINYINT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
DECLARE @UserEmail NVARCHAR(256)
|
||||
SELECT @UserEmail = Email
|
||||
FROM
|
||||
[dbo].[UserView]
|
||||
WHERE
|
||||
Id = @UserId
|
||||
|
||||
;WITH OrgUsers AS
|
||||
(
|
||||
-- All users except invited (Status <> 0): direct UserId match
|
||||
SELECT
|
||||
OU.[Id],
|
||||
OU.[OrganizationId],
|
||||
OU.[Type],
|
||||
OU.[Status],
|
||||
OU.[Permissions]
|
||||
FROM
|
||||
[dbo].[OrganizationUserView] OU
|
||||
WHERE
|
||||
OU.[Status] <> 0
|
||||
AND OU.[UserId] = @UserId
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Invited users: email match
|
||||
SELECT
|
||||
OU.[Id],
|
||||
OU.[OrganizationId],
|
||||
OU.[Type],
|
||||
OU.[Status],
|
||||
OU.[Permissions]
|
||||
FROM
|
||||
[dbo].[OrganizationUserView] OU
|
||||
WHERE
|
||||
OU.[Status] = 0
|
||||
AND OU.[Email] = @UserEmail
|
||||
AND @UserEmail IS NOT NULL
|
||||
),
|
||||
Providers AS
|
||||
(
|
||||
SELECT
|
||||
OrganizationId
|
||||
FROM
|
||||
[dbo].[UserProviderAccessView]
|
||||
WHERE
|
||||
UserId = @UserId
|
||||
)
|
||||
SELECT
|
||||
OU.[Id] AS [OrganizationUserId],
|
||||
P.[OrganizationId],
|
||||
P.[Type] AS [PolicyType],
|
||||
P.[Enabled] AS [PolicyEnabled],
|
||||
P.[Data] AS [PolicyData],
|
||||
OU.[Type] AS [OrganizationUserType],
|
||||
OU.[Status] AS [OrganizationUserStatus],
|
||||
OU.[Permissions] AS [OrganizationUserPermissionsData],
|
||||
CASE WHEN PR.[OrganizationId] IS NULL THEN 0 ELSE 1 END AS [IsProvider]
|
||||
FROM
|
||||
[dbo].[PolicyView] P
|
||||
INNER JOIN
|
||||
OrgUsers OU ON P.[OrganizationId] = OU.[OrganizationId]
|
||||
LEFT JOIN
|
||||
Providers PR ON PR.[OrganizationId] = OU.[OrganizationId]
|
||||
WHERE
|
||||
P.[Type] = @PolicyType
|
||||
END
|
||||
GO
|
||||
Reference in New Issue
Block a user