mirror of
https://github.com/bitwarden/server.git
synced 2025-05-28 23:04:50 -05:00
[PM-14439] Add PolicyRequirementQuery for enforcement logic (#5336)
* Add PolicyRequirementQuery, helpers and models in preparation for migrating domain code Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com>
This commit is contained in:
parent
54d59b3b92
commit
f4341b2f3b
@ -0,0 +1,39 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an OrganizationUser and a Policy which *may* be enforced against them.
|
||||||
|
/// You may assume that the Policy is enabled and that the organization's plan supports policies.
|
||||||
|
/// This is consumed by <see cref="IPolicyRequirement"/> to create requirements for specific policy types.
|
||||||
|
/// </summary>
|
||||||
|
public class PolicyDetails
|
||||||
|
{
|
||||||
|
public Guid OrganizationUserId { get; set; }
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
|
public PolicyType PolicyType { get; set; }
|
||||||
|
public string? PolicyData { get; set; }
|
||||||
|
public OrganizationUserType OrganizationUserType { get; set; }
|
||||||
|
public OrganizationUserStatusType OrganizationUserStatus { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Custom permissions for the organization user, if any. Use <see cref="GetOrganizationUserCustomPermissions"/>
|
||||||
|
/// to deserialize.
|
||||||
|
/// </summary>
|
||||||
|
public string? OrganizationUserPermissionsData { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// True if the user is also a ProviderUser for the organization, false otherwise.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsProvider { get; set; }
|
||||||
|
|
||||||
|
public T GetDataModel<T>() where T : IPolicyDataModel, new()
|
||||||
|
=> CoreHelpers.LoadClassFromJsonData<T>(PolicyData);
|
||||||
|
|
||||||
|
public Permissions GetOrganizationUserCustomPermissions()
|
||||||
|
=> CoreHelpers.LoadClassFromJsonData<Permissions>(OrganizationUserPermissionsData);
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
|
|
||||||
|
public interface IPolicyRequirementQuery
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get a policy requirement for a specific user.
|
||||||
|
/// The policy requirement represents how one or more policy types should be enforced against the user.
|
||||||
|
/// It will always return a value even if there are no policies that should be enforced.
|
||||||
|
/// This should be used for all policy checks.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The user that you need to enforce the policy against.</param>
|
||||||
|
/// <typeparam name="T">The IPolicyRequirement that corresponds to the policy you want to enforce.</typeparam>
|
||||||
|
Task<T> GetAsync<T>(Guid userId) where T : IPolicyRequirement;
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||||
|
|
||||||
|
public class PolicyRequirementQuery(
|
||||||
|
IPolicyRepository policyRepository,
|
||||||
|
IEnumerable<RequirementFactory<IPolicyRequirement>> factories)
|
||||||
|
: IPolicyRequirementQuery
|
||||||
|
{
|
||||||
|
public async Task<T> GetAsync<T>(Guid userId) where T : IPolicyRequirement
|
||||||
|
{
|
||||||
|
var factory = factories.OfType<RequirementFactory<T>>().SingleOrDefault();
|
||||||
|
if (factory is null)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException("No Policy Requirement found for " + typeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
return factory(await GetPolicyDetails(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<IEnumerable<PolicyDetails>> GetPolicyDetails(Guid userId) =>
|
||||||
|
policyRepository.GetPolicyDetailsByUserId(userId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the business requirements of how one or more enterprise policies will be enforced against a user.
|
||||||
|
/// The implementation of this interface will depend on how the policies are enforced in the relevant domain.
|
||||||
|
/// </summary>
|
||||||
|
public interface IPolicyRequirement;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A factory function that takes a sequence of <see cref="PolicyDetails"/> and transforms them into a single
|
||||||
|
/// <see cref="IPolicyRequirement"/> for consumption by the relevant domain. This will receive *all* policy types
|
||||||
|
/// that may be enforced against a user; when implementing this delegate, you must filter out irrelevant policy types
|
||||||
|
/// as well as policies that should not be enforced against a user (e.g. due to the user's role or status).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// See <see cref="PolicyRequirementHelpers"/> for extension methods to handle common requirements when implementing
|
||||||
|
/// this delegate.
|
||||||
|
/// </remarks>
|
||||||
|
public delegate T RequirementFactory<out T>(IEnumerable<PolicyDetails> policyDetails)
|
||||||
|
where T : IPolicyRequirement;
|
@ -0,0 +1,41 @@
|
|||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
|
||||||
|
public static class PolicyRequirementHelpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Filters the PolicyDetails by PolicyType. This is generally required to only get the PolicyDetails that your
|
||||||
|
/// IPolicyRequirement relates to.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<PolicyDetails> GetPolicyType(
|
||||||
|
this IEnumerable<PolicyDetails> policyDetails,
|
||||||
|
PolicyType type)
|
||||||
|
=> policyDetails.Where(x => x.PolicyType == type);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filters the PolicyDetails to remove the specified user roles. This can be used to exempt
|
||||||
|
/// owners and admins from policy enforcement.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<PolicyDetails> ExemptRoles(
|
||||||
|
this IEnumerable<PolicyDetails> policyDetails,
|
||||||
|
IEnumerable<OrganizationUserType> roles)
|
||||||
|
=> policyDetails.Where(x => !roles.Contains(x.OrganizationUserType));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filters the PolicyDetails to remove organization users who are also provider users for the organization.
|
||||||
|
/// This can be used to exempt provider users from policy enforcement.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<PolicyDetails> ExemptProviders(this IEnumerable<PolicyDetails> policyDetails)
|
||||||
|
=> policyDetails.Where(x => !x.IsProvider);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filters the PolicyDetails to remove the specified organization user statuses. For example, this can be used
|
||||||
|
/// to exempt users in the invited and revoked statuses from policy enforcement.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<PolicyDetails> ExemptStatus(
|
||||||
|
this IEnumerable<PolicyDetails> policyDetails, IEnumerable<OrganizationUserStatusType> status)
|
||||||
|
=> policyDetails.Where(x => !status.Contains(x.OrganizationUserStatus));
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.AdminConsole.Services.Implementations;
|
using Bit.Core.AdminConsole.Services.Implementations;
|
||||||
@ -12,7 +13,14 @@ public static class PolicyServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
services.AddScoped<IPolicyService, PolicyService>();
|
services.AddScoped<IPolicyService, PolicyService>();
|
||||||
services.AddScoped<ISavePolicyCommand, SavePolicyCommand>();
|
services.AddScoped<ISavePolicyCommand, SavePolicyCommand>();
|
||||||
|
services.AddScoped<IPolicyRequirementQuery, PolicyRequirementQuery>();
|
||||||
|
|
||||||
|
services.AddPolicyValidators();
|
||||||
|
services.AddPolicyRequirements();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddPolicyValidators(this IServiceCollection services)
|
||||||
|
{
|
||||||
services.AddScoped<IPolicyValidator, TwoFactorAuthenticationPolicyValidator>();
|
services.AddScoped<IPolicyValidator, TwoFactorAuthenticationPolicyValidator>();
|
||||||
services.AddScoped<IPolicyValidator, SingleOrgPolicyValidator>();
|
services.AddScoped<IPolicyValidator, SingleOrgPolicyValidator>();
|
||||||
services.AddScoped<IPolicyValidator, RequireSsoPolicyValidator>();
|
services.AddScoped<IPolicyValidator, RequireSsoPolicyValidator>();
|
||||||
@ -20,4 +28,34 @@ public static class PolicyServiceCollectionExtensions
|
|||||||
services.AddScoped<IPolicyValidator, MaximumVaultTimeoutPolicyValidator>();
|
services.AddScoped<IPolicyValidator, MaximumVaultTimeoutPolicyValidator>();
|
||||||
services.AddScoped<IPolicyValidator, FreeFamiliesForEnterprisePolicyValidator>();
|
services.AddScoped<IPolicyValidator, FreeFamiliesForEnterprisePolicyValidator>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AddPolicyRequirements(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
// Register policy requirement factories here
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to register simple policy requirements where its factory method implements CreateRequirement.
|
||||||
|
/// This MUST be used rather than calling AddScoped directly, because it will ensure the factory method has
|
||||||
|
/// the correct type to be injected and then identified by <see cref="PolicyRequirementQuery"/> at runtime.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The specific PolicyRequirement being registered.</typeparam>
|
||||||
|
private static void AddPolicyRequirement<T>(this IServiceCollection serviceCollection, RequirementFactory<T> factory)
|
||||||
|
where T : class, IPolicyRequirement
|
||||||
|
=> serviceCollection.AddPolicyRequirement(_ => factory);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to register policy requirements where you need to access additional dependencies (usually to return a
|
||||||
|
/// curried factory method).
|
||||||
|
/// This MUST be used rather than calling AddScoped directly, because it will ensure the factory method has
|
||||||
|
/// the correct type to be injected and then identified by <see cref="PolicyRequirementQuery"/> at runtime.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">
|
||||||
|
/// A callback that takes IServiceProvider and returns a RequirementFactory for
|
||||||
|
/// your policy requirement.
|
||||||
|
/// </typeparam>
|
||||||
|
private static void AddPolicyRequirement<T>(this IServiceCollection serviceCollection,
|
||||||
|
Func<IServiceProvider, RequirementFactory<T>> factory)
|
||||||
|
where T : class, IPolicyRequirement
|
||||||
|
=> serviceCollection.AddScoped<RequirementFactory<IPolicyRequirement>>(factory);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
@ -8,7 +10,25 @@ namespace Bit.Core.AdminConsole.Repositories;
|
|||||||
|
|
||||||
public interface IPolicyRepository : IRepository<Policy, Guid>
|
public interface IPolicyRepository : IRepository<Policy, Guid>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all policies of a given type for an organization.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// WARNING: do not use this to enforce policies against a user! It returns raw data and does not take into account
|
||||||
|
/// various business rules. Use <see cref="IPolicyRequirementQuery"/> instead.
|
||||||
|
/// </remarks>
|
||||||
Task<Policy?> GetByOrganizationIdTypeAsync(Guid organizationId, PolicyType type);
|
Task<Policy?> GetByOrganizationIdTypeAsync(Guid organizationId, PolicyType type);
|
||||||
Task<ICollection<Policy>> GetManyByOrganizationIdAsync(Guid organizationId);
|
Task<ICollection<Policy>> GetManyByOrganizationIdAsync(Guid organizationId);
|
||||||
Task<ICollection<Policy>> GetManyByUserIdAsync(Guid userId);
|
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);
|
||||||
}
|
}
|
||||||
|
@ -110,6 +110,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission";
|
public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission";
|
||||||
public const string ShortcutDuplicatePatchRequests = "pm-16812-shortcut-duplicate-patch-requests";
|
public const string ShortcutDuplicatePatchRequests = "pm-16812-shortcut-duplicate-patch-requests";
|
||||||
public const string PushSyncOrgKeysOnRevokeRestore = "pm-17168-push-sync-org-keys-on-revoke-restore";
|
public const string PushSyncOrgKeysOnRevokeRestore = "pm-17168-push-sync-org-keys-on-revoke-restore";
|
||||||
|
public const string PolicyRequirements = "pm-14439-policy-requirements";
|
||||||
|
|
||||||
/* Tools Team */
|
/* Tools Team */
|
||||||
public const string ItemShare = "item-share";
|
public const string ItemShare = "item-share";
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System.Data;
|
using System.Data;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Infrastructure.Dapper.Repositories;
|
using Bit.Infrastructure.Dapper.Repositories;
|
||||||
@ -59,4 +60,17 @@ public class PolicyRepository : Repository<Policy, Guid>, IPolicyRepository
|
|||||||
return results.ToList();
|
return results.ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries;
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries;
|
||||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
@ -50,4 +52,43 @@ public class PolicyRepository : Repository<AdminConsoleEntities.Policy, Policy,
|
|||||||
return Mapper.Map<List<AdminConsoleEntities.Policy>>(results);
|
return Mapper.Map<List<AdminConsoleEntities.Policy>>(results);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
43
src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByUserId.sql
Normal file
43
src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByUserId.sql
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[PolicyDetails_ReadByUserId]
|
||||||
|
@UserId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
SELECT
|
||||||
|
OU.[Id] AS OrganizationUserId,
|
||||||
|
P.[OrganizationId],
|
||||||
|
P.[Type] AS PolicyType,
|
||||||
|
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]
|
||||||
|
INNER JOIN [dbo].[OrganizationView] O
|
||||||
|
ON P.[OrganizationId] = O.[Id]
|
||||||
|
WHERE
|
||||||
|
P.Enabled = 1
|
||||||
|
AND O.Enabled = 1
|
||||||
|
AND O.UsePolicies = 1
|
||||||
|
AND (
|
||||||
|
-- OrgUsers who have accepted their invite and are linked to a UserId
|
||||||
|
-- (Note: this excludes "invited but revoked" users who don't have an OU.UserId yet,
|
||||||
|
-- but those users will go through policy enforcement later as part of accepting their invite after being restored.
|
||||||
|
-- This is an intentionally unhandled edge case for now.)
|
||||||
|
(OU.[Status] != 0 AND OU.[UserId] = @UserId)
|
||||||
|
|
||||||
|
-- 'Invited' OrgUsers are not linked to a UserId yet, so we have to look up their email
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM [dbo].[UserView] U
|
||||||
|
WHERE U.[Id] = @UserId AND OU.[Email] = U.[Email] AND OU.[Status] = 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
END
|
@ -0,0 +1,60 @@
|
|||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class PolicyRequirementQueryTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that the query correctly registers, retrieves and instantiates arbitrary IPolicyRequirements
|
||||||
|
/// according to their provided CreateRequirement delegate.
|
||||||
|
/// </summary>
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetAsync_Works(Guid userId, Guid organizationId)
|
||||||
|
{
|
||||||
|
var policyRepository = Substitute.For<IPolicyRepository>();
|
||||||
|
var factories = new List<RequirementFactory<IPolicyRequirement>>
|
||||||
|
{
|
||||||
|
// In prod this cast is handled when the CreateRequirement delegate is registered in DI
|
||||||
|
(RequirementFactory<TestPolicyRequirement>)TestPolicyRequirement.Create
|
||||||
|
};
|
||||||
|
|
||||||
|
var sut = new PolicyRequirementQuery(policyRepository, factories);
|
||||||
|
policyRepository.GetPolicyDetailsByUserId(userId).Returns([
|
||||||
|
new PolicyDetails
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
var requirement = await sut.GetAsync<TestPolicyRequirement>(userId);
|
||||||
|
Assert.Equal(organizationId, requirement.OrganizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetAsync_ThrowsIfNoRequirementRegistered(Guid userId)
|
||||||
|
{
|
||||||
|
var policyRepository = Substitute.For<IPolicyRepository>();
|
||||||
|
var sut = new PolicyRequirementQuery(policyRepository, []);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<NotImplementedException>(()
|
||||||
|
=> sut.GetAsync<TestPolicyRequirement>(userId));
|
||||||
|
Assert.Contains("No Policy Requirement found", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Intentionally simplified PolicyRequirement that just holds the Policy.OrganizationId for us to assert against.
|
||||||
|
/// </summary>
|
||||||
|
private class TestPolicyRequirement : IPolicyRequirement
|
||||||
|
{
|
||||||
|
public Guid OrganizationId { get; init; }
|
||||||
|
public static TestPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
|
||||||
|
=> new() { OrganizationId = policyDetails.Single().OrganizationId };
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,385 @@
|
|||||||
|
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
|
||||||
|
{
|
||||||
|
[DatabaseTheory, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[DatabaseTheory, 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
[DatabaseTheory, 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
[DatabaseTheory, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[DatabaseTheory, 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
[DatabaseTheory, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[DatabaseTheory, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[DatabaseTheory, 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,43 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE [dbo].[PolicyDetails_ReadByUserId]
|
||||||
|
@UserId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
SELECT
|
||||||
|
OU.[Id] AS OrganizationUserId,
|
||||||
|
P.[OrganizationId],
|
||||||
|
P.[Type] AS PolicyType,
|
||||||
|
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]
|
||||||
|
INNER JOIN [dbo].[OrganizationView] O
|
||||||
|
ON P.[OrganizationId] = O.[Id]
|
||||||
|
WHERE
|
||||||
|
P.Enabled = 1
|
||||||
|
AND O.Enabled = 1
|
||||||
|
AND O.UsePolicies = 1
|
||||||
|
AND (
|
||||||
|
-- OrgUsers who have accepted their invite and are linked to a UserId
|
||||||
|
-- (Note: this excludes "invited but revoked" users who don't have an OU.UserId yet,
|
||||||
|
-- but those users will go through policy enforcement later as part of accepting their invite after being restored.
|
||||||
|
-- This is an intentionally unhandled edge case for now.)
|
||||||
|
(OU.[Status] != 0 AND OU.[UserId] = @UserId)
|
||||||
|
|
||||||
|
-- 'Invited' OrgUsers are not linked to a UserId yet, so we have to look up their email
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM [dbo].[UserView] U
|
||||||
|
WHERE U.[Id] = @UserId AND OU.[Email] = U.[Email] AND OU.[Status] = 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
END
|
Loading…
x
Reference in New Issue
Block a user