From 8701e203a56c92767e3fb1cf415b1afebac7e009 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Mon, 27 Jan 2025 16:41:12 +1000 Subject: [PATCH 01/20] Add requirements --- .../MasterPasswordRequirement.cs | 23 ++++++++++ .../PersonalOwnershipRequirement.cs | 20 +++++++++ .../PolicyRequirementHelpers.cs | 29 ++++++++++++ .../SendRequirement.cs | 31 +++++++++++++ .../SingleOrgPolicyRequirement.cs | 45 +++++++++++++++++++ .../SsoRequirement.cs | 23 ++++++++++ .../TwoFactorAuthenticationRequirement.cs | 26 +++++++++++ 7 files changed, 197 insertions(+) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/MasterPasswordRequirement.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/PersonalOwnershipRequirement.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/PolicyRequirementHelpers.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/SendRequirement.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/SingleOrgPolicyRequirement.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/SsoRequirement.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/TwoFactorAuthenticationRequirement.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/MasterPasswordRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/MasterPasswordRequirement.cs new file mode 100644 index 000000000000..2947476cf4c1 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/MasterPasswordRequirement.cs @@ -0,0 +1,23 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; + +public class MasterPasswordRequirement : MasterPasswordPolicyData +{ + public static MasterPasswordRequirement Create(IEnumerable userPolicyDetails) => + userPolicyDetails + .GetPolicyType(PolicyType.MasterPassword) + .ExcludeProviders() + .ExcludeRevokedAndInvitedUsers() + .Select(up => up.GetDataModel()) + .Aggregate( + new MasterPasswordRequirement(), + (result, current) => + { + result.CombineWith(current); + return result; + } + ); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/PersonalOwnershipRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/PersonalOwnershipRequirement.cs new file mode 100644 index 000000000000..81ab8201a643 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/PersonalOwnershipRequirement.cs @@ -0,0 +1,20 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; + +public class PersonalOwnershipRequirement +{ + public bool DisablePersonalOwnership { get; init; } + + public static PersonalOwnershipRequirement Create(IEnumerable userPolicyDetails) + => new() + { + DisablePersonalOwnership = userPolicyDetails + .GetPolicyType(PolicyType.PersonalOwnership) + .ExcludeOwnersAndAdmins() + .ExcludeProviders() + .ExcludeRevokedAndInvitedUsers() + .Any() + }; +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/PolicyRequirementHelpers.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/PolicyRequirementHelpers.cs new file mode 100644 index 000000000000..2c2115b4af9a --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/PolicyRequirementHelpers.cs @@ -0,0 +1,29 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; + +public static class PolicyRequirementHelpers +{ + public static IEnumerable GetPolicyType( + this IEnumerable userPolicyDetails, + PolicyType type) => + userPolicyDetails.Where(x => x.PolicyType == type); + + public static IEnumerable ExcludeOwnersAndAdmins( + this IEnumerable userPolicyDetails) => + userPolicyDetails.Where(x => x.OrganizationUserType != OrganizationUserType.Owner); + + public static IEnumerable ExcludeProviders( + this IEnumerable userPolicyDetails) => + userPolicyDetails.Where(x => !x.IsProvider); + + public static IEnumerable ExcludeRevokedAndInvitedUsers( + this IEnumerable userPolicyDetails) => + userPolicyDetails.Where(x => x.OrganizationUserStatus >= OrganizationUserStatusType.Accepted); + + public static IEnumerable ExcludeRevokedUsers( + this IEnumerable userPolicyDetails) => + userPolicyDetails.Where(x => x.OrganizationUserStatus >= OrganizationUserStatusType.Invited); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/SendRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/SendRequirement.cs new file mode 100644 index 000000000000..4a0a9d39abc0 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/SendRequirement.cs @@ -0,0 +1,31 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; + +public class SendRequirement +{ + public bool DisableSend { get; init; } + public bool DisableHideEmail { get; init; } + + public static SendRequirement Create(IEnumerable userPolicyDetails) + { + var filteredPolicies = userPolicyDetails + .ExcludeOwnersAndAdmins() + .ExcludeRevokedAndInvitedUsers() + .ToList(); + + return new SendRequirement + { + DisableSend = filteredPolicies + .GetPolicyType(PolicyType.DisableSend) + .Any(), + + DisableHideEmail = filteredPolicies + .GetPolicyType(PolicyType.SendOptions) + .Select(up => up.GetDataModel()) + .Any(d => d.DisableHideEmail) + }; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/SingleOrgPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/SingleOrgPolicyRequirement.cs new file mode 100644 index 000000000000..e442b48767df --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/SingleOrgPolicyRequirement.cs @@ -0,0 +1,45 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; + +public enum SingleOrganizationRequirementResult +{ + Ok = 1, + RequiredByThisOrganization = 2, + RequiredByOtherOrganization = 3 +} + +public class SingleOrganizationRequirement +{ + private IEnumerable PolicyDetails { get; } + + public SingleOrganizationRequirement(IEnumerable userPolicyDetails) + { + PolicyDetails = userPolicyDetails + .GetPolicyType(PolicyType.SingleOrg) + .ExcludeOwnersAndAdmins() + .ExcludeProviders() + .ToList(); + } + + public SingleOrganizationRequirementResult CanJoinOrganization(Guid organizationId) + { + // Check for the org the user is trying to join + if (PolicyDetails.Any(x => x.OrganizationId == organizationId)) + { + return SingleOrganizationRequirementResult.RequiredByThisOrganization; + } + + // Check for other orgs the user might already be a member of (accepted or confirmed status only) + if (PolicyDetails.ExcludeRevokedAndInvitedUsers().Any()) + { + return SingleOrganizationRequirementResult.RequiredByOtherOrganization; + } + + return SingleOrganizationRequirementResult.Ok; + } + + public SingleOrganizationRequirementResult CanBeRestoredToOrganization(Guid organizationId) => + CanJoinOrganization(organizationId); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/SsoRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/SsoRequirement.cs new file mode 100644 index 000000000000..5aeff56dc9b5 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/SsoRequirement.cs @@ -0,0 +1,23 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Settings; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; + +public class SsoRequirement +{ + public bool RequireSso { get; init; } + + public static SsoRequirement Create( + IEnumerable userPolicyDetails, + IGlobalSettings globalSettings) + => new() + { + RequireSso = userPolicyDetails + .GetPolicyType(PolicyType.RequireSso) + .ExcludeProviders() + // TODO: confirm minStatus - maybe confirmed? + .ExcludeRevokedAndInvitedUsers() + .Any(up => !up.IsAdminType() || globalSettings.Sso.EnforceSsoPolicyForAllUsers) + }; +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/TwoFactorAuthenticationRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/TwoFactorAuthenticationRequirement.cs new file mode 100644 index 000000000000..1475a119cd21 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/TwoFactorAuthenticationRequirement.cs @@ -0,0 +1,26 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; + +public class TwoFactorAuthenticationRequirement +{ + private IEnumerable PolicyDetails { get; } + + public TwoFactorAuthenticationRequirement(IEnumerable userPolicyDetails) + { + PolicyDetails = userPolicyDetails + .GetPolicyType(PolicyType.TwoFactorAuthentication) + .ExcludeOwnersAndAdmins() + .ExcludeProviders() + .ToList(); + } + + public bool RequiredToJoinOrganization(Guid organizationId) + => PolicyDetails.Any(x => x.OrganizationId == organizationId); + + public IEnumerable OrganizationsRequiringTwoFactor + => PolicyDetails + .ExcludeRevokedAndInvitedUsers() + .Select(x => x.OrganizationId); +} From d0bcb2b1dcbf223e34fd57829cd3fd38bfd1c5d5 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Mon, 27 Jan 2025 16:46:13 +1000 Subject: [PATCH 02/20] Add draft sql query --- .../PolicyDetails_ReadByUserId.sql | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByUserId.sql diff --git a/src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByUserId.sql b/src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByUserId.sql new file mode 100644 index 000000000000..bbee838a28bf --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByUserId.sql @@ -0,0 +1,37 @@ +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 ( + (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 + ) + ) +END From 010d6b8e7970c357dca8dd267eba2fb202271b6a Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Mon, 27 Jan 2025 17:32:11 +1000 Subject: [PATCH 03/20] Simple query without generics --- .../OrganizationUserPolicyDetails.cs | 10 ++++++++-- .../Policies/IPolicyRequirementQuery.cs | 9 +++++++++ .../Implementations/PolicyRequirementQuery.cs | 17 +++++++++++++++++ .../MasterPasswordRequirement.cs | 0 .../PersonalOwnershipRequirement.cs | 0 .../PolicyRequirementHelpers.cs | 0 .../SendRequirement.cs | 0 .../SingleOrganizationRequirement.cs} | 0 .../SsoRequirement.cs | 5 ++++- .../TwoFactorAuthenticationRequirement.cs | 0 .../Repositories/IPolicyRepository.cs | 2 ++ 11 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs rename src/Core/AdminConsole/OrganizationFeatures/Policies/{PolicyRequirementQueries => PolicyRequirements}/MasterPasswordRequirement.cs (100%) rename src/Core/AdminConsole/OrganizationFeatures/Policies/{PolicyRequirementQueries => PolicyRequirements}/PersonalOwnershipRequirement.cs (100%) rename src/Core/AdminConsole/OrganizationFeatures/Policies/{PolicyRequirementQueries => PolicyRequirements}/PolicyRequirementHelpers.cs (100%) rename src/Core/AdminConsole/OrganizationFeatures/Policies/{PolicyRequirementQueries => PolicyRequirements}/SendRequirement.cs (100%) rename src/Core/AdminConsole/OrganizationFeatures/Policies/{PolicyRequirementQueries/SingleOrgPolicyRequirement.cs => PolicyRequirements/SingleOrganizationRequirement.cs} (100%) rename src/Core/AdminConsole/OrganizationFeatures/Policies/{PolicyRequirementQueries => PolicyRequirements}/SsoRequirement.cs (75%) rename src/Core/AdminConsole/OrganizationFeatures/Policies/{PolicyRequirementQueries => PolicyRequirements}/TwoFactorAuthenticationRequirement.cs (100%) diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs index 84939ecf791e..f2814f1ec8f8 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs @@ -1,5 +1,7 @@ using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.Enums; +using Bit.Core.Utilities; namespace Bit.Core.Models.Data.Organizations.OrganizationUsers; @@ -13,8 +15,6 @@ public class OrganizationUserPolicyDetails public bool PolicyEnabled { get; set; } - public string PolicyData { get; set; } - public OrganizationUserType OrganizationUserType { get; set; } public OrganizationUserStatusType OrganizationUserStatus { get; set; } @@ -22,4 +22,10 @@ public class OrganizationUserPolicyDetails public string OrganizationUserPermissionsData { get; set; } public bool IsProvider { get; set; } + private string PolicyData { get; set; } + + public T GetDataModel() where T : IPolicyDataModel, new() + { + return CoreHelpers.LoadClassFromJsonData(PolicyData); + } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs new file mode 100644 index 000000000000..4fd8935c29fb --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs @@ -0,0 +1,9 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies; + +public interface IPolicyRequirementQuery +{ + Task GetSingleOrganizationRequirementAsync(Guid userId); + Task GetSendRequirementAsync(Guid userId); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs new file mode 100644 index 000000000000..6f9165b30c7b --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs @@ -0,0 +1,17 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; + +public class PolicyRequirementQuery(IPolicyRepository policyRepository) : IPolicyRequirementQuery +{ + public async Task GetSingleOrganizationRequirementAsync(Guid userId) + => new(await GetPolicyDetails(userId)); + + public async Task GetSendRequirementAsync(Guid userId) + => SendRequirement.Create(await GetPolicyDetails(userId)); + + private Task> GetPolicyDetails(Guid userId) => + policyRepository.GetPolicyDetailsByUserId(userId); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/MasterPasswordRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordRequirement.cs similarity index 100% rename from src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/MasterPasswordRequirement.cs rename to src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordRequirement.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/PersonalOwnershipRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipRequirement.cs similarity index 100% rename from src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/PersonalOwnershipRequirement.cs rename to src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipRequirement.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/PolicyRequirementHelpers.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs similarity index 100% rename from src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/PolicyRequirementHelpers.cs rename to src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/SendRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendRequirement.cs similarity index 100% rename from src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/SendRequirement.cs rename to src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendRequirement.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/SingleOrgPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationRequirement.cs similarity index 100% rename from src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/SingleOrgPolicyRequirement.cs rename to src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationRequirement.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/SsoRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoRequirement.cs similarity index 75% rename from src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/SsoRequirement.cs rename to src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoRequirement.cs index 5aeff56dc9b5..fde75e04c64a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/SsoRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoRequirement.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.Enums; +using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Settings; @@ -18,6 +19,8 @@ public static SsoRequirement Create( .ExcludeProviders() // TODO: confirm minStatus - maybe confirmed? .ExcludeRevokedAndInvitedUsers() - .Any(up => !up.IsAdminType() || globalSettings.Sso.EnforceSsoPolicyForAllUsers) + .Any(up => + up.OrganizationUserType is not OrganizationUserType.Owner and not OrganizationUserType.Admin || + globalSettings.Sso.EnforceSsoPolicyForAllUsers) }; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/TwoFactorAuthenticationRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/TwoFactorAuthenticationRequirement.cs similarity index 100% rename from src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueries/TwoFactorAuthenticationRequirement.cs rename to src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/TwoFactorAuthenticationRequirement.cs diff --git a/src/Core/AdminConsole/Repositories/IPolicyRepository.cs b/src/Core/AdminConsole/Repositories/IPolicyRepository.cs index ad0654dd3c55..62a72ec3d725 100644 --- a/src/Core/AdminConsole/Repositories/IPolicyRepository.cs +++ b/src/Core/AdminConsole/Repositories/IPolicyRepository.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; #nullable enable @@ -11,4 +12,5 @@ public interface IPolicyRepository : IRepository Task GetByOrganizationIdTypeAsync(Guid organizationId, PolicyType type); Task> GetManyByOrganizationIdAsync(Guid organizationId); Task> GetManyByUserIdAsync(Guid userId); + Task> GetPolicyDetailsByUserId(Guid userId); } From 9fa1096647463f209c3d70cf9643766e346e7264 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Wed, 29 Jan 2025 09:00:25 +1000 Subject: [PATCH 04/20] Build out query --- .../Implementations/PolicyRequirementQuery.cs | 44 ++++++++++++++++--- .../PolicyRequirements/SendRequirement.cs | 3 +- .../PolicyRequirements/SsoRequirement.cs | 7 +-- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs index 6f9165b30c7b..bfd31f1877fb 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs @@ -1,17 +1,49 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Settings; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; -public class PolicyRequirementQuery(IPolicyRepository policyRepository) : IPolicyRequirementQuery +public interface IRequirement; + +public delegate T CreateRequirement(IEnumerable userPolicyDetails) + where T : IRequirement; + +public class PolicyRequirementQuery : IPolicyRequirementQuery { - public async Task GetSingleOrganizationRequirementAsync(Guid userId) - => new(await GetPolicyDetails(userId)); + private readonly IPolicyRepository _policyRepository; + + private readonly Dictionary, IRequirement>> _registry = new(); + + public PolicyRequirementQuery(IGlobalSettings globalSettings, IPolicyRepository policyRepository) + { + _policyRepository = policyRepository; - public async Task GetSendRequirementAsync(Guid userId) - => SendRequirement.Create(await GetPolicyDetails(userId)); + // Register Requirement factory functions below + Register(SendRequirement.Create); + Register(up => SsoRequirement.Create(up, globalSettings.Sso)); + } + + public async Task GetAsync(Guid userId) where T : IRequirement => GetRequirementFactory()(await GetPolicyDetails(userId)); private Task> GetPolicyDetails(Guid userId) => - policyRepository.GetPolicyDetailsByUserId(userId); + _policyRepository.GetPolicyDetailsByUserId(userId); + + private void Register(Func, T> factory) where T : IRequirement + { + IRequirement Converted(IEnumerable up) => factory(up); + _registry.Add(typeof(T), Converted); + } + + private Func, T> GetRequirementFactory() where T : IRequirement + { + if (!_registry.TryGetValue(typeof(T), out var factory)) + { + throw new NotImplementedException("No Policy Requirement found for " + typeof(T)); + } + + T Converted(IEnumerable up) => (T)factory(up); + return Converted; + } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendRequirement.cs index 4a0a9d39abc0..26e6432d28ba 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendRequirement.cs @@ -1,10 +1,11 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; -public class SendRequirement +public class SendRequirement : IRequirement { public bool DisableSend { get; init; } public bool DisableHideEmail { get; init; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoRequirement.cs index fde75e04c64a..c7158e3ada98 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoRequirement.cs @@ -1,17 +1,18 @@ using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Settings; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; -public class SsoRequirement +public class SsoRequirement : IRequirement { public bool RequireSso { get; init; } public static SsoRequirement Create( IEnumerable userPolicyDetails, - IGlobalSettings globalSettings) + ISsoSettings ssoSettings) => new() { RequireSso = userPolicyDetails @@ -21,6 +22,6 @@ public static SsoRequirement Create( .ExcludeRevokedAndInvitedUsers() .Any(up => up.OrganizationUserType is not OrganizationUserType.Owner and not OrganizationUserType.Admin || - globalSettings.Sso.EnforceSsoPolicyForAllUsers) + ssoSettings.EnforceSsoPolicyForAllUsers) }; } From b3977d4630db25b977f6f8c3d136fb50d4f788be Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Wed, 29 Jan 2025 09:28:55 +1000 Subject: [PATCH 05/20] Use delegate, wire up --- .../OrganizationUserPolicyDetails.cs | 2 +- .../Policies/IPolicyRequirementQuery.cs | 5 ++--- .../Implementations/PolicyRequirementQuery.cs | 7 +++--- .../MasterPasswordRequirement.cs | 5 +++-- .../PersonalOwnershipRequirement.cs | 5 +++-- .../PolicyRequirementHelpers.cs | 2 +- .../PolicyRequirements/SendRequirement.cs | 2 +- .../SingleOrganizationRequirement.cs | 5 +++-- .../PolicyRequirements/SsoRequirement.cs | 1 + .../TwoFactorAuthenticationRequirement.cs | 5 +++-- .../PolicyServiceCollectionExtensions.cs | 1 + .../Services/Implementations/SendService.cs | 22 +++++++++---------- .../Repositories/PolicyRepository.cs | 14 ++++++++++++ .../Repositories/PolicyRepository.cs | 3 +++ 14 files changed, 50 insertions(+), 29 deletions(-) diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs index f2814f1ec8f8..9d381915cdda 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs @@ -14,6 +14,7 @@ public class OrganizationUserPolicyDetails public PolicyType PolicyType { get; set; } public bool PolicyEnabled { get; set; } + public string PolicyData { get; set; } public OrganizationUserType OrganizationUserType { get; set; } @@ -22,7 +23,6 @@ public class OrganizationUserPolicyDetails public string OrganizationUserPermissionsData { get; set; } public bool IsProvider { get; set; } - private string PolicyData { get; set; } public T GetDataModel() where T : IPolicyDataModel, new() { diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs index 4fd8935c29fb..87cd90c0b70f 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs @@ -1,9 +1,8 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies; public interface IPolicyRequirementQuery { - Task GetSingleOrganizationRequirementAsync(Guid userId); - Task GetSendRequirementAsync(Guid userId); + Task GetAsync(Guid userId) where T : IRequirement; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs index bfd31f1877fb..4498ea0a647d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Settings; @@ -14,7 +15,7 @@ public class PolicyRequirementQuery : IPolicyRequirementQuery { private readonly IPolicyRepository _policyRepository; - private readonly Dictionary, IRequirement>> _registry = new(); + private readonly Dictionary> _registry = new(); public PolicyRequirementQuery(IGlobalSettings globalSettings, IPolicyRepository policyRepository) { @@ -30,13 +31,13 @@ public PolicyRequirementQuery(IGlobalSettings globalSettings, IPolicyRepository private Task> GetPolicyDetails(Guid userId) => _policyRepository.GetPolicyDetailsByUserId(userId); - private void Register(Func, T> factory) where T : IRequirement + private void Register(CreateRequirement factory) where T : IRequirement { IRequirement Converted(IEnumerable up) => factory(up); _registry.Add(typeof(T), Converted); } - private Func, T> GetRequirementFactory() where T : IRequirement + private CreateRequirement GetRequirementFactory() where T : IRequirement { if (!_registry.TryGetValue(typeof(T), out var factory)) { diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordRequirement.cs index 2947476cf4c1..89fc8db10311 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordRequirement.cs @@ -1,10 +1,11 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; -namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; -public class MasterPasswordRequirement : MasterPasswordPolicyData +public class MasterPasswordRequirement : MasterPasswordPolicyData, IRequirement { public static MasterPasswordRequirement Create(IEnumerable userPolicyDetails) => userPolicyDetails diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipRequirement.cs index 81ab8201a643..f2cf0bcc5953 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipRequirement.cs @@ -1,9 +1,10 @@ using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; -namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; -public class PersonalOwnershipRequirement +public class PersonalOwnershipRequirement : IRequirement { public bool DisablePersonalOwnership { get; init; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs index 2c2115b4af9a..54caf1b6eebb 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs @@ -2,7 +2,7 @@ using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations.OrganizationUsers; -namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; public static class PolicyRequirementHelpers { diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendRequirement.cs index 26e6432d28ba..2389d1429694 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendRequirement.cs @@ -3,7 +3,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; -namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; public class SendRequirement : IRequirement { diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationRequirement.cs index e442b48767df..d951f7a4f099 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationRequirement.cs @@ -1,7 +1,8 @@ using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; -namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; public enum SingleOrganizationRequirementResult { @@ -10,7 +11,7 @@ public enum SingleOrganizationRequirementResult RequiredByOtherOrganization = 3 } -public class SingleOrganizationRequirement +public class SingleOrganizationRequirement : IRequirement { private IEnumerable PolicyDetails { get; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoRequirement.cs index c7158e3ada98..8d80bdc8005b 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoRequirement.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Settings; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/TwoFactorAuthenticationRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/TwoFactorAuthenticationRequirement.cs index 1475a119cd21..1dbd8bee1b9c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/TwoFactorAuthenticationRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/TwoFactorAuthenticationRequirement.cs @@ -1,9 +1,10 @@ using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; -namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; -public class TwoFactorAuthenticationRequirement +public class TwoFactorAuthenticationRequirement : IRequirement { private IEnumerable PolicyDetails { get; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index 4e88976c1001..a18deabd23e9 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -12,6 +12,7 @@ public static void AddPolicyServices(this IServiceCollection services) { services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Core/Tools/Services/Implementations/SendService.cs b/src/Core/Tools/Services/Implementations/SendService.cs index 918379d7a504..533e8b131250 100644 --- a/src/Core/Tools/Services/Implementations/SendService.cs +++ b/src/Core/Tools/Services/Implementations/SendService.cs @@ -1,6 +1,6 @@ using System.Text.Json; -using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Context; @@ -36,6 +36,7 @@ public class SendService : ISendService private readonly IReferenceEventService _referenceEventService; private readonly GlobalSettings _globalSettings; private readonly ICurrentContext _currentContext; + private readonly IPolicyRequirementQuery _policyRequirementQuery; private const long _fileSizeLeeway = 1024L * 1024L; // 1MB public SendService( @@ -50,7 +51,8 @@ public SendService( GlobalSettings globalSettings, IPolicyRepository policyRepository, IPolicyService policyService, - ICurrentContext currentContext) + ICurrentContext currentContext, + IPolicyRequirementQuery policyRequirementQuery) { _sendRepository = sendRepository; _userRepository = userRepository; @@ -64,6 +66,7 @@ public SendService( _referenceEventService = referenceEventService; _globalSettings = globalSettings; _currentContext = currentContext; + _policyRequirementQuery = policyRequirementQuery; } public async Task SaveSendAsync(Send send) @@ -291,20 +294,15 @@ private async Task ValidateUserCanSaveAsync(Guid? userId, Send send) return; } - var anyDisableSendPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId.Value, - PolicyType.DisableSend); - if (anyDisableSendPolicies) + var sendRequirement = await _policyRequirementQuery.GetAsync(userId.Value); + if (sendRequirement.DisableSend) { throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); } - if (send.HideEmail.GetValueOrDefault()) + if (send.HideEmail.GetValueOrDefault() && sendRequirement.DisableHideEmail) { - var sendOptionsPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId.Value, PolicyType.SendOptions); - if (sendOptionsPolicies.Any(p => CoreHelpers.LoadClassFromJsonData(p.PolicyData)?.DisableHideEmail ?? false)) - { - throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send."); - } + throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send."); } } diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs index 196f3e3733f0..2d136cb875b4 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs @@ -2,6 +2,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Settings; using Bit.Infrastructure.Dapper.Repositories; using Dapper; @@ -59,4 +60,17 @@ public async Task> GetManyByUserIdAsync(Guid userId) return results.ToList(); } } + + public async Task> GetPolicyDetailsByUserId(Guid userId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[PolicyDetails_ReadByUserId]", + new { UserId = userId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs index 3eb4ac934bf9..a7e6f2ce0ba2 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs @@ -1,6 +1,7 @@ using AutoMapper; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries; using Bit.Infrastructure.EntityFramework.Repositories; @@ -50,4 +51,6 @@ public PolicyRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper return Mapper.Map>(results); } } + + public Task> GetPolicyDetailsByUserId(Guid userId) => throw new NotImplementedException(); } From 983eb70c9583e1500b228234660c51ae0238b3a9 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Wed, 29 Jan 2025 11:37:07 +1000 Subject: [PATCH 06/20] Tweak naming, use extension methods --- .../OrganizationUserPolicyDetails.cs | 1 + .../Policies/IPolicyRequirementQuery.cs | 4 +- .../Implementations/PolicyRequirementQuery.cs | 46 +++++++++++++------ .../PolicyRequirements/IPolicyRequirement.cs | 8 ++++ .../MasterPasswordRequirement.cs | 7 ++- .../PersonalOwnershipRequirement.cs | 5 +- .../PolicyRequirements/SendRequirement.cs | 7 ++- .../SingleOrganizationRequirement.cs | 5 +- ...Requirement.cs => SsoPolicyRequirement.cs} | 5 +- .../TwoFactorAuthenticationRequirement.cs | 5 +- .../Services/Implementations/SendService.cs | 2 +- 11 files changed, 57 insertions(+), 38 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs rename src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/{SsoRequirement.cs => SsoPolicyRequirement.cs} (85%) diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs index 9d381915cdda..68270c45e04c 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs @@ -14,6 +14,7 @@ public class OrganizationUserPolicyDetails public PolicyType PolicyType { get; set; } public bool PolicyEnabled { get; set; } + public string PolicyData { get; set; } public OrganizationUserType OrganizationUserType { get; set; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs index 87cd90c0b70f..35821dd9d1f5 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs @@ -1,8 +1,8 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies; public interface IPolicyRequirementQuery { - Task GetAsync(Guid userId) where T : IRequirement; + Task GetAsync(Guid userId) where T : IPolicyRequirement; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs index 4498ea0a647d..6e581c0cfece 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs @@ -6,44 +6,60 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; -public interface IRequirement; - -public delegate T CreateRequirement(IEnumerable userPolicyDetails) - where T : IRequirement; - public class PolicyRequirementQuery : IPolicyRequirementQuery { private readonly IPolicyRepository _policyRepository; - private readonly Dictionary> _registry = new(); + /// + /// A dictionary associating a Policy Requirement's type with its factory function. + /// + private readonly IReadOnlyDictionary> _policyRequirements; public PolicyRequirementQuery(IGlobalSettings globalSettings, IPolicyRepository policyRepository) { _policyRepository = policyRepository; + var policyRequirements = new Dictionary>(); + + // Register Policy Requirement factory functions below + policyRequirements.AddRequirement(SendPolicyRequirement.Create); + policyRequirements.AddRequirement(up + => SsoPolicyRequirement.Create(up, globalSettings.Sso)); - // Register Requirement factory functions below - Register(SendRequirement.Create); - Register(up => SsoRequirement.Create(up, globalSettings.Sso)); + _policyRequirements = policyRequirements.AsReadOnly(); } - public async Task GetAsync(Guid userId) where T : IRequirement => GetRequirementFactory()(await GetPolicyDetails(userId)); + public async Task GetAsync(Guid userId) where T : IPolicyRequirement + => _policyRequirements.GetRequirement()(await GetPolicyDetails(userId)); private Task> GetPolicyDetails(Guid userId) => _policyRepository.GetPolicyDetailsByUserId(userId); +} - private void Register(CreateRequirement factory) where T : IRequirement +/// +/// Extension methods used to add and retrieve IPolicyRequirements in the dictionary by their specific type. +/// +internal static class PolicyRequirementDictionaryExtensions +{ + public static void AddRequirement( + this IDictionary> registry, + CreateRequirement factory) where T : IPolicyRequirement { - IRequirement Converted(IEnumerable up) => factory(up); - _registry.Add(typeof(T), Converted); + // Explicitly convert T to an IPolicyRequirement (C# doesn't do this automatically). + IPolicyRequirement Converted(IEnumerable up) => factory(up); + registry.Add(typeof(T), Converted); } - private CreateRequirement GetRequirementFactory() where T : IRequirement + public static CreateRequirement GetRequirement( + this IReadOnlyDictionary> registry) where T : IPolicyRequirement { - if (!_registry.TryGetValue(typeof(T), out var factory)) + if (!registry.TryGetValue(typeof(T), out var factory)) { throw new NotImplementedException("No Policy Requirement found for " + typeof(T)); } + // Explicitly convert IPolicyRequirement back to T (C# doesn't do this automatically). + // The cast here relies on the Register method correctly associating the type and factory function. T Converted(IEnumerable up) => (T)factory(up); return Converted; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs new file mode 100644 index 000000000000..afec98cc4079 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs @@ -0,0 +1,8 @@ +using Bit.Core.Models.Data.Organizations.OrganizationUsers; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +public interface IPolicyRequirement; + +public delegate T CreateRequirement(IEnumerable userPolicyDetails) + where T : IPolicyRequirement; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordRequirement.cs index 89fc8db10311..17245a031bc3 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordRequirement.cs @@ -1,20 +1,19 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; -public class MasterPasswordRequirement : MasterPasswordPolicyData, IRequirement +public class MasterPasswordPolicyRequirement : MasterPasswordPolicyData, IPolicyRequirement { - public static MasterPasswordRequirement Create(IEnumerable userPolicyDetails) => + public static MasterPasswordPolicyRequirement Create(IEnumerable userPolicyDetails) => userPolicyDetails .GetPolicyType(PolicyType.MasterPassword) .ExcludeProviders() .ExcludeRevokedAndInvitedUsers() .Select(up => up.GetDataModel()) .Aggregate( - new MasterPasswordRequirement(), + new MasterPasswordPolicyRequirement(), (result, current) => { result.CombineWith(current); diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipRequirement.cs index f2cf0bcc5953..e56f3d586036 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipRequirement.cs @@ -1,14 +1,13 @@ using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; -public class PersonalOwnershipRequirement : IRequirement +public class PersonalOwnershipPolicyRequirement : IPolicyRequirement { public bool DisablePersonalOwnership { get; init; } - public static PersonalOwnershipRequirement Create(IEnumerable userPolicyDetails) + public static PersonalOwnershipPolicyRequirement Create(IEnumerable userPolicyDetails) => new() { DisablePersonalOwnership = userPolicyDetails diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendRequirement.cs index 2389d1429694..db62d982c6bc 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendRequirement.cs @@ -1,23 +1,22 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; -public class SendRequirement : IRequirement +public class SendPolicyRequirement : IPolicyRequirement { public bool DisableSend { get; init; } public bool DisableHideEmail { get; init; } - public static SendRequirement Create(IEnumerable userPolicyDetails) + public static SendPolicyRequirement Create(IEnumerable userPolicyDetails) { var filteredPolicies = userPolicyDetails .ExcludeOwnersAndAdmins() .ExcludeRevokedAndInvitedUsers() .ToList(); - return new SendRequirement + return new SendPolicyRequirement { DisableSend = filteredPolicies .GetPolicyType(PolicyType.DisableSend) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationRequirement.cs index d951f7a4f099..66b268442ec1 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationRequirement.cs @@ -1,5 +1,4 @@ using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; @@ -11,11 +10,11 @@ public enum SingleOrganizationRequirementResult RequiredByOtherOrganization = 3 } -public class SingleOrganizationRequirement : IRequirement +public class SingleOrganizationPolicyRequirement : IPolicyRequirement { private IEnumerable PolicyDetails { get; } - public SingleOrganizationRequirement(IEnumerable userPolicyDetails) + public SingleOrganizationPolicyRequirement(IEnumerable userPolicyDetails) { PolicyDetails = userPolicyDetails .GetPolicyType(PolicyType.SingleOrg) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoPolicyRequirement.cs similarity index 85% rename from src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoRequirement.cs rename to src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoPolicyRequirement.cs index 8d80bdc8005b..0aac581473ec 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoPolicyRequirement.cs @@ -1,5 +1,4 @@ using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations.OrganizationUsers; @@ -7,11 +6,11 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; -public class SsoRequirement : IRequirement +public class SsoPolicyRequirement : IPolicyRequirement { public bool RequireSso { get; init; } - public static SsoRequirement Create( + public static SsoPolicyRequirement Create( IEnumerable userPolicyDetails, ISsoSettings ssoSettings) => new() diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/TwoFactorAuthenticationRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/TwoFactorAuthenticationRequirement.cs index 1dbd8bee1b9c..dc568d96214a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/TwoFactorAuthenticationRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/TwoFactorAuthenticationRequirement.cs @@ -1,14 +1,13 @@ using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; -public class TwoFactorAuthenticationRequirement : IRequirement +public class TwoFactorAuthenticationPolicyRequirement : IPolicyRequirement { private IEnumerable PolicyDetails { get; } - public TwoFactorAuthenticationRequirement(IEnumerable userPolicyDetails) + public TwoFactorAuthenticationPolicyRequirement(IEnumerable userPolicyDetails) { PolicyDetails = userPolicyDetails .GetPolicyType(PolicyType.TwoFactorAuthentication) diff --git a/src/Core/Tools/Services/Implementations/SendService.cs b/src/Core/Tools/Services/Implementations/SendService.cs index 533e8b131250..19e8ce4c1c84 100644 --- a/src/Core/Tools/Services/Implementations/SendService.cs +++ b/src/Core/Tools/Services/Implementations/SendService.cs @@ -294,7 +294,7 @@ private async Task ValidateUserCanSaveAsync(Guid? userId, Send send) return; } - var sendRequirement = await _policyRequirementQuery.GetAsync(userId.Value); + var sendRequirement = await _policyRequirementQuery.GetAsync(userId.Value); if (sendRequirement.DisableSend) { throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); From 6f5df04a2c95cdf85419c2db61f56ce5bf24610c Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Wed, 29 Jan 2025 11:43:14 +1000 Subject: [PATCH 07/20] Wrap dict in separate class --- .../Implementations/PolicyRequirementQuery.cs | 63 +++++++++---------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs index 6e581c0cfece..d9269419d625 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs @@ -9,58 +9,53 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; public class PolicyRequirementQuery : IPolicyRequirementQuery { private readonly IPolicyRepository _policyRepository; - - /// - /// A dictionary associating a Policy Requirement's type with its factory function. - /// - private readonly IReadOnlyDictionary> _policyRequirements; + private readonly PolicyRequirementRegistry _policyRequirements = new(); public PolicyRequirementQuery(IGlobalSettings globalSettings, IPolicyRepository policyRepository) { _policyRepository = policyRepository; - var policyRequirements = new Dictionary>(); // Register Policy Requirement factory functions below - policyRequirements.AddRequirement(SendPolicyRequirement.Create); - policyRequirements.AddRequirement(up + _policyRequirements.Add(SendPolicyRequirement.Create); + _policyRequirements.Add(up => SsoPolicyRequirement.Create(up, globalSettings.Sso)); - - _policyRequirements = policyRequirements.AsReadOnly(); } public async Task GetAsync(Guid userId) where T : IPolicyRequirement - => _policyRequirements.GetRequirement()(await GetPolicyDetails(userId)); + => _policyRequirements.Get()(await GetPolicyDetails(userId)); private Task> GetPolicyDetails(Guid userId) => _policyRepository.GetPolicyDetailsByUserId(userId); -} -/// -/// Extension methods used to add and retrieve IPolicyRequirements in the dictionary by their specific type. -/// -internal static class PolicyRequirementDictionaryExtensions -{ - public static void AddRequirement( - this IDictionary> registry, - CreateRequirement factory) where T : IPolicyRequirement + /// + /// Helper class used to register and retrieve Policy Requirement factories by type. + /// + private class PolicyRequirementRegistry { - // Explicitly convert T to an IPolicyRequirement (C# doesn't do this automatically). - IPolicyRequirement Converted(IEnumerable up) => factory(up); - registry.Add(typeof(T), Converted); - } + /// + /// A dictionary associating a Policy Requirement's type with its factory function. + /// + private readonly Dictionary> _registry = new(); - public static CreateRequirement GetRequirement( - this IReadOnlyDictionary> registry) where T : IPolicyRequirement - { - if (!registry.TryGetValue(typeof(T), out var factory)) + public void Add(CreateRequirement factory) where T : IPolicyRequirement { - throw new NotImplementedException("No Policy Requirement found for " + typeof(T)); + // Explicitly convert T to an IPolicyRequirement (C# doesn't do this automatically). + IPolicyRequirement Converted(IEnumerable up) => factory(up); + _registry.Add(typeof(T), Converted); } - // Explicitly convert IPolicyRequirement back to T (C# doesn't do this automatically). - // The cast here relies on the Register method correctly associating the type and factory function. - T Converted(IEnumerable up) => (T)factory(up); - return Converted; + public CreateRequirement Get() where T : IPolicyRequirement + { + if (!_registry.TryGetValue(typeof(T), out var factory)) + { + throw new NotImplementedException("No Policy Requirement found for " + typeof(T)); + } + + // Explicitly convert IPolicyRequirement back to T (C# doesn't do this automatically). + // The cast here relies on the Register method correctly associating the type and factory function. + T Converted(IEnumerable up) => (T)factory(up); + return Converted; + } } } + From 205dde293011d261504b48b96a42fddff5c237d6 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Wed, 29 Jan 2025 11:44:35 +1000 Subject: [PATCH 08/20] Remove duplicate comment --- .../Policies/Implementations/PolicyRequirementQuery.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs index d9269419d625..0bb473f32053 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs @@ -32,9 +32,6 @@ private Task> GetPolicyDetails(Guid u /// private class PolicyRequirementRegistry { - /// - /// A dictionary associating a Policy Requirement's type with its factory function. - /// private readonly Dictionary> _registry = new(); public void Add(CreateRequirement factory) where T : IPolicyRequirement From e335e30d3bbfd46e8c0ec1421ba5dda17d999bf6 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Wed, 29 Jan 2025 13:12:35 +1000 Subject: [PATCH 09/20] fix linting and style --- .../Policies/Implementations/PolicyRequirementQuery.cs | 3 +-- ...sswordRequirement.cs => MasterPasswordPolicyRequirement.cs} | 0 ...hipRequirement.cs => PersonalOwnershipPolicyRequirement.cs} | 0 .../{SendRequirement.cs => SendPolicyRequirement.cs} | 0 ...onRequirement.cs => SingleOrganizationPolicyRequirement.cs} | 0 .../Policies/PolicyRequirements/SsoPolicyRequirement.cs | 3 +-- ...uirement.cs => TwoFactorAuthenticationPolicyRequirement.cs} | 0 7 files changed, 2 insertions(+), 4 deletions(-) rename src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/{MasterPasswordRequirement.cs => MasterPasswordPolicyRequirement.cs} (100%) rename src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/{PersonalOwnershipRequirement.cs => PersonalOwnershipPolicyRequirement.cs} (100%) rename src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/{SendRequirement.cs => SendPolicyRequirement.cs} (100%) rename src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/{SingleOrganizationRequirement.cs => SingleOrganizationPolicyRequirement.cs} (100%) rename src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/{TwoFactorAuthenticationRequirement.cs => TwoFactorAuthenticationPolicyRequirement.cs} (100%) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs index 0bb473f32053..864f4a96610e 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs @@ -1,5 +1,4 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Settings; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordPolicyRequirement.cs similarity index 100% rename from src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordRequirement.cs rename to src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordPolicyRequirement.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs similarity index 100% rename from src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipRequirement.cs rename to src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs similarity index 100% rename from src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendRequirement.cs rename to src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs similarity index 100% rename from src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationRequirement.cs rename to src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoPolicyRequirement.cs index 0aac581473ec..c4e9d756ae10 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoPolicyRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoPolicyRequirement.cs @@ -1,10 +1,9 @@ using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Settings; -namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirementQueries; +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; public class SsoPolicyRequirement : IPolicyRequirement { diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/TwoFactorAuthenticationRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/TwoFactorAuthenticationPolicyRequirement.cs similarity index 100% rename from src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/TwoFactorAuthenticationRequirement.cs rename to src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/TwoFactorAuthenticationPolicyRequirement.cs From 3f7de0f082496dfc5648f706f3a2410dbf6019fb Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Fri, 7 Feb 2025 11:46:48 +1000 Subject: [PATCH 10/20] Revert other policy requirements for MVP, tweak single org --- .../Implementations/PolicyRequirementQuery.cs | 6 ++-- .../MasterPasswordPolicyRequirement.cs | 23 ------------ .../PersonalOwnershipPolicyRequirement.cs | 20 ----------- .../SendPolicyRequirement.cs | 31 ---------------- .../SingleOrganizationPolicyRequirement.cs | 36 +++++++++++-------- .../SsoPolicyRequirement.cs | 26 -------------- ...woFactorAuthenticationPolicyRequirement.cs | 26 -------------- .../Services/Implementations/SendService.cs | 22 ++++++------ 8 files changed, 35 insertions(+), 155 deletions(-) delete mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordPolicyRequirement.cs delete mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs delete mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs delete mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoPolicyRequirement.cs delete mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/TwoFactorAuthenticationPolicyRequirement.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs index 864f4a96610e..4914024a68ab 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs @@ -10,14 +10,12 @@ public class PolicyRequirementQuery : IPolicyRequirementQuery private readonly IPolicyRepository _policyRepository; private readonly PolicyRequirementRegistry _policyRequirements = new(); - public PolicyRequirementQuery(IGlobalSettings globalSettings, IPolicyRepository policyRepository) + public PolicyRequirementQuery(IPolicyRepository policyRepository) { _policyRepository = policyRepository; // Register Policy Requirement factory functions below - _policyRequirements.Add(SendPolicyRequirement.Create); - _policyRequirements.Add(up - => SsoPolicyRequirement.Create(up, globalSettings.Sso)); + _policyRequirements.Add(SingleOrganizationPolicyRequirement.Create); } public async Task GetAsync(Guid userId) where T : IPolicyRequirement diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordPolicyRequirement.cs deleted file mode 100644 index 17245a031bc3..000000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordPolicyRequirement.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; - -namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; - -public class MasterPasswordPolicyRequirement : MasterPasswordPolicyData, IPolicyRequirement -{ - public static MasterPasswordPolicyRequirement Create(IEnumerable userPolicyDetails) => - userPolicyDetails - .GetPolicyType(PolicyType.MasterPassword) - .ExcludeProviders() - .ExcludeRevokedAndInvitedUsers() - .Select(up => up.GetDataModel()) - .Aggregate( - new MasterPasswordPolicyRequirement(), - (result, current) => - { - result.CombineWith(current); - return result; - } - ); -} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs deleted file mode 100644 index e56f3d586036..000000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Bit.Core.AdminConsole.Enums; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; - -namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; - -public class PersonalOwnershipPolicyRequirement : IPolicyRequirement -{ - public bool DisablePersonalOwnership { get; init; } - - public static PersonalOwnershipPolicyRequirement Create(IEnumerable userPolicyDetails) - => new() - { - DisablePersonalOwnership = userPolicyDetails - .GetPolicyType(PolicyType.PersonalOwnership) - .ExcludeOwnersAndAdmins() - .ExcludeProviders() - .ExcludeRevokedAndInvitedUsers() - .Any() - }; -} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs deleted file mode 100644 index db62d982c6bc..000000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; - -namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; - -public class SendPolicyRequirement : IPolicyRequirement -{ - public bool DisableSend { get; init; } - public bool DisableHideEmail { get; init; } - - public static SendPolicyRequirement Create(IEnumerable userPolicyDetails) - { - var filteredPolicies = userPolicyDetails - .ExcludeOwnersAndAdmins() - .ExcludeRevokedAndInvitedUsers() - .ToList(); - - return new SendPolicyRequirement - { - DisableSend = filteredPolicies - .GetPolicyType(PolicyType.DisableSend) - .Any(), - - DisableHideEmail = filteredPolicies - .GetPolicyType(PolicyType.SendOptions) - .Select(up => up.GetDataModel()) - .Any(d => d.DisableHideEmail) - }; - } -} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs index 66b268442ec1..cad652eef952 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.Enums; +using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; @@ -6,35 +7,40 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements public enum SingleOrganizationRequirementResult { Ok = 1, - RequiredByThisOrganization = 2, - RequiredByOtherOrganization = 3 + BlockedByThisOrganization = 2, + BlockedByOtherOrganization = 3 } public class SingleOrganizationPolicyRequirement : IPolicyRequirement { - private IEnumerable PolicyDetails { get; } - - public SingleOrganizationPolicyRequirement(IEnumerable userPolicyDetails) - { - PolicyDetails = userPolicyDetails - .GetPolicyType(PolicyType.SingleOrg) - .ExcludeOwnersAndAdmins() - .ExcludeProviders() - .ToList(); - } + /// + /// Single organization policy details filtered by user role but not status. + /// This lets us check whether a user is compliant before being accepted/restored. + /// + private IEnumerable PolicyDetails { get; init; } + + public static SingleOrganizationPolicyRequirement Create(IEnumerable userPolicyDetails) + => new() + { + PolicyDetails = userPolicyDetails + .GetPolicyType(PolicyType.SingleOrg) + .ExcludeOwnersAndAdmins() + .ExcludeProviders() + .ToList() + }; public SingleOrganizationRequirementResult CanJoinOrganization(Guid organizationId) { - // Check for the org the user is trying to join + // Check for the org the user is trying to join; status doesn't matter if (PolicyDetails.Any(x => x.OrganizationId == organizationId)) { - return SingleOrganizationRequirementResult.RequiredByThisOrganization; + return SingleOrganizationRequirementResult.BlockedByThisOrganization; } // Check for other orgs the user might already be a member of (accepted or confirmed status only) if (PolicyDetails.ExcludeRevokedAndInvitedUsers().Any()) { - return SingleOrganizationRequirementResult.RequiredByOtherOrganization; + return SingleOrganizationRequirementResult.BlockedByOtherOrganization; } return SingleOrganizationRequirementResult.Ok; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoPolicyRequirement.cs deleted file mode 100644 index c4e9d756ae10..000000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SsoPolicyRequirement.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Bit.Core.AdminConsole.Enums; -using Bit.Core.Enums; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; -using Bit.Core.Settings; - -namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; - -public class SsoPolicyRequirement : IPolicyRequirement -{ - public bool RequireSso { get; init; } - - public static SsoPolicyRequirement Create( - IEnumerable userPolicyDetails, - ISsoSettings ssoSettings) - => new() - { - RequireSso = userPolicyDetails - .GetPolicyType(PolicyType.RequireSso) - .ExcludeProviders() - // TODO: confirm minStatus - maybe confirmed? - .ExcludeRevokedAndInvitedUsers() - .Any(up => - up.OrganizationUserType is not OrganizationUserType.Owner and not OrganizationUserType.Admin || - ssoSettings.EnforceSsoPolicyForAllUsers) - }; -} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/TwoFactorAuthenticationPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/TwoFactorAuthenticationPolicyRequirement.cs deleted file mode 100644 index dc568d96214a..000000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/TwoFactorAuthenticationPolicyRequirement.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Bit.Core.AdminConsole.Enums; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; - -namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; - -public class TwoFactorAuthenticationPolicyRequirement : IPolicyRequirement -{ - private IEnumerable PolicyDetails { get; } - - public TwoFactorAuthenticationPolicyRequirement(IEnumerable userPolicyDetails) - { - PolicyDetails = userPolicyDetails - .GetPolicyType(PolicyType.TwoFactorAuthentication) - .ExcludeOwnersAndAdmins() - .ExcludeProviders() - .ToList(); - } - - public bool RequiredToJoinOrganization(Guid organizationId) - => PolicyDetails.Any(x => x.OrganizationId == organizationId); - - public IEnumerable OrganizationsRequiringTwoFactor - => PolicyDetails - .ExcludeRevokedAndInvitedUsers() - .Select(x => x.OrganizationId); -} diff --git a/src/Core/Tools/Services/Implementations/SendService.cs b/src/Core/Tools/Services/Implementations/SendService.cs index 19e8ce4c1c84..918379d7a504 100644 --- a/src/Core/Tools/Services/Implementations/SendService.cs +++ b/src/Core/Tools/Services/Implementations/SendService.cs @@ -1,6 +1,6 @@ using System.Text.Json; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Context; @@ -36,7 +36,6 @@ public class SendService : ISendService private readonly IReferenceEventService _referenceEventService; private readonly GlobalSettings _globalSettings; private readonly ICurrentContext _currentContext; - private readonly IPolicyRequirementQuery _policyRequirementQuery; private const long _fileSizeLeeway = 1024L * 1024L; // 1MB public SendService( @@ -51,8 +50,7 @@ public SendService( GlobalSettings globalSettings, IPolicyRepository policyRepository, IPolicyService policyService, - ICurrentContext currentContext, - IPolicyRequirementQuery policyRequirementQuery) + ICurrentContext currentContext) { _sendRepository = sendRepository; _userRepository = userRepository; @@ -66,7 +64,6 @@ public SendService( _referenceEventService = referenceEventService; _globalSettings = globalSettings; _currentContext = currentContext; - _policyRequirementQuery = policyRequirementQuery; } public async Task SaveSendAsync(Send send) @@ -294,15 +291,20 @@ private async Task ValidateUserCanSaveAsync(Guid? userId, Send send) return; } - var sendRequirement = await _policyRequirementQuery.GetAsync(userId.Value); - if (sendRequirement.DisableSend) + var anyDisableSendPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId.Value, + PolicyType.DisableSend); + if (anyDisableSendPolicies) { throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); } - if (send.HideEmail.GetValueOrDefault() && sendRequirement.DisableHideEmail) + if (send.HideEmail.GetValueOrDefault()) { - throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send."); + var sendOptionsPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId.Value, PolicyType.SendOptions); + if (sendOptionsPolicies.Any(p => CoreHelpers.LoadClassFromJsonData(p.PolicyData)?.DisableHideEmail ?? false)) + { + throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send."); + } } } From 2049cc8cb37a1ca7e9ea1ca435020af2b14973c8 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Fri, 7 Feb 2025 12:21:47 +1000 Subject: [PATCH 11/20] Call new query in accept org user flow --- .../OrganizationUserPolicyDetails.cs | 2 +- .../OrganizationUsers/AcceptOrgUserCommand.cs | 82 ++++++++++--- src/Core/Constants.cs | 1 + .../Services/Implementations/UserService.cs | 2 +- .../AcceptOrgUserCommandTests.cs | 109 +++++++++++++++++- 5 files changed, 173 insertions(+), 23 deletions(-) diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs index 68270c45e04c..584ca02a6f24 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs @@ -7,7 +7,7 @@ namespace Bit.Core.Models.Data.Organizations.OrganizationUsers; public class OrganizationUserPolicyDetails { - public Guid OrganizationUserId { get; set; } + public Guid? OrganizationUserId { get; set; } public Guid OrganizationId { get; set; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs index 756bd2ae46d7..136a4ab020cd 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs @@ -1,4 +1,6 @@ using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Billing.Enums; @@ -13,7 +15,7 @@ using Bit.Core.Utilities; using Microsoft.AspNetCore.DataProtection; -namespace Bit.Core.OrganizationFeatures.OrganizationUsers; +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; public class AcceptOrgUserCommand : IAcceptOrgUserCommand { @@ -25,6 +27,8 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand private readonly IMailService _mailService; private readonly IUserRepository _userRepository; private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; + private readonly IPolicyRequirementQuery _policyRequirementQuery; + private readonly IFeatureService _featureService; public AcceptOrgUserCommand( IDataProtectionProvider dataProtectionProvider, @@ -34,7 +38,9 @@ public AcceptOrgUserCommand( IPolicyService policyService, IMailService mailService, IUserRepository userRepository, - IDataProtectorTokenFactory orgUserInviteTokenDataFactory) + IDataProtectorTokenFactory orgUserInviteTokenDataFactory, + IPolicyRequirementQuery policyRequirementQuery, + IFeatureService featureService) { // TODO: remove data protector when old token validation removed @@ -46,8 +52,13 @@ public AcceptOrgUserCommand( _mailService = mailService; _userRepository = userRepository; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; + _policyRequirementQuery = policyRequirementQuery; + _featureService = featureService; } + public const string ErrorBlockedByThisSingleOrganizationPolicy = "You may not join this organization until you leave or remove all other organizations."; + public const string ErrorBlockedByOtherSingleOrganizationPolicy = "You cannot join this organization because you are a member of another organization which forbids it"; + public async Task AcceptOrgUserByEmailTokenAsync(Guid organizationUserId, User user, string emailToken, IUserService userService) { @@ -172,7 +183,35 @@ public async Task AcceptOrgUserAsync(OrganizationUser orgUser, } } - // Enforce Single Organization Policy of organization user is trying to join + if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) + { + await EnforcePolicies_vNext(user, orgUser, userService); + } + else + { + await EnforcePolicies(user, orgUser, userService); + } + + orgUser.Status = OrganizationUserStatusType.Accepted; + orgUser.UserId = user.Id; + orgUser.Email = null; + + await _organizationUserRepository.ReplaceAsync(orgUser); + + var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(orgUser.OrganizationId, OrganizationUserType.Admin); + var adminEmails = admins.Select(a => a.Email).Distinct().ToList(); + + if (adminEmails.Count > 0) + { + var organization = await _organizationRepository.GetByIdAsync(orgUser.OrganizationId); + await _mailService.SendOrganizationAcceptedEmailAsync(organization, user.Email, adminEmails); + } + + return orgUser; + } + + private async Task EnforcePolicies(User user, OrganizationUser orgUser, IUserService userService) + { var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(user.Id); var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId); var invitedSingleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, @@ -201,23 +240,34 @@ public async Task AcceptOrgUserAsync(OrganizationUser orgUser, throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account."); } } + } - orgUser.Status = OrganizationUserStatusType.Accepted; - orgUser.UserId = user.Id; - orgUser.Email = null; - - await _organizationUserRepository.ReplaceAsync(orgUser); - - var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(orgUser.OrganizationId, OrganizationUserType.Admin); - var adminEmails = admins.Select(a => a.Email).Distinct().ToList(); + private async Task EnforcePolicies_vNext(User user, OrganizationUser orgUser, IUserService userService) + { + var singleOrganizationRequirement = + await _policyRequirementQuery.GetAsync(user.Id); - if (adminEmails.Count > 0) + switch (singleOrganizationRequirement.CanJoinOrganization(orgUser.OrganizationId)) { - var organization = await _organizationRepository.GetByIdAsync(orgUser.OrganizationId); - await _mailService.SendOrganizationAcceptedEmailAsync(organization, user.Email, adminEmails); + case SingleOrganizationRequirementResult.BlockedByThisOrganization: + throw new BadRequestException(ErrorBlockedByThisSingleOrganizationPolicy); + case SingleOrganizationRequirementResult.BlockedByOtherOrganization: + throw new BadRequestException(ErrorBlockedByOtherSingleOrganizationPolicy); + case SingleOrganizationRequirementResult.Ok: + default: + break; } - return orgUser; + // TODO: this will be rewritten once a requirement is added + // Enforce Two Factor Authentication Policy of organization user is trying to join + if (!await userService.TwoFactorIsEnabledAsync(user)) + { + var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, + PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); + if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) + { + throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account."); + } + } } - } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 3e59a9390bad..4aa80f165a74 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -109,6 +109,7 @@ public static class FeatureFlagKeys public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications"; public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission"; public const string PushSyncOrgKeysOnRevokeRestore = "pm-17168-push-sync-org-keys-on-revoke-restore"; + public const string PolicyRequirements = "pm-14439-policy-requirements"; /* Tools Team */ public const string ItemShare = "item-share"; diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 11d4042defb8..bafb2c640992 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1372,7 +1372,7 @@ private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user) await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync( new RevokeOrganizationUsersRequest( p.OrganizationId, - [new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }], + [new OrganizationUserUserDetails { Id = p.OrganizationUserId!.Value, OrganizationId = p.OrganizationId }], new SystemUser(EventSystemUser.TwoFactorDisabled))); await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email); } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs index 2dda23481a81..4c1ea4464fa3 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs @@ -1,5 +1,8 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Billing.Enums; @@ -7,7 +10,6 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations.OrganizationUsers; -using Bit.Core.OrganizationFeatures.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -131,6 +133,37 @@ public async Task AcceptOrgUser_UserJoiningOrgWithSingleOrgPolicyWhileInAnotherO exception.Message); } + [Theory] + [BitAutoData] + public async Task AcceptOrgUser_UserJoiningOrgWithSingleOrgPolicyWhileInAnotherOrg_ThrowsBadRequest_vNext( + SutProvider sutProvider, + User user, Organization org, OrganizationUser orgUser) + { + // Arrange + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + ArrangeOrgUser(orgUser, user, org); + + // Make organization they are trying to join have the single org policy + var singleOrganizationRequirement = SingleOrganizationPolicyRequirement.Create([ + new OrganizationUserPolicyDetails + { + OrganizationId = org.Id, + OrganizationUserStatus = OrganizationUserStatusType.Invited, + OrganizationUserType = OrganizationUserType.User, + PolicyEnabled = true, + PolicyType = PolicyType.SingleOrg + } + ]); + sutProvider.GetDependency().GetAsync(user.Id) + .Returns(singleOrganizationRequirement); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService)); + + Assert.Equal(AcceptOrgUserCommand.ErrorBlockedByThisSingleOrganizationPolicy, exception.Message); + } + [Theory] [BitAutoData] public async Task AcceptOrgUserAsync_UserInOrgWithSingleOrgPolicyAlready_ThrowsBadRequest( @@ -154,6 +187,37 @@ public async Task AcceptOrgUserAsync_UserInOrgWithSingleOrgPolicyAlready_ThrowsB exception.Message); } + [Theory] + [BitAutoData] + public async Task AcceptOrgUserAsync_UserInOrgWithSingleOrgPolicyAlready_ThrowsBadRequest_vNext( + SutProvider sutProvider, + User user, Organization org, OrganizationUser orgUser) + { + // Arrange + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + ArrangeOrgUser(orgUser, user, org); + + // Mock that user is part of an org that has the single org policy + var singleOrganizationRequirement = SingleOrganizationPolicyRequirement.Create([ + new OrganizationUserPolicyDetails + { + OrganizationId = Guid.NewGuid(), + OrganizationUserId = Guid.NewGuid(), + OrganizationUserStatus = OrganizationUserStatusType.Accepted, + OrganizationUserType = OrganizationUserType.User, + PolicyEnabled = true, + PolicyType = PolicyType.SingleOrg + } + ]); + sutProvider.GetDependency().GetAsync(user.Id) + .Returns(singleOrganizationRequirement); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService)); + + Assert.Equal(AcceptOrgUserCommand.ErrorBlockedByOtherSingleOrganizationPolicy, exception.Message); + } [Theory] [BitAutoData] @@ -183,6 +247,37 @@ public async Task AcceptOrgUserAsync_UserWithout2FAJoining2FARequiredOrg_ThrowsB exception.Message); } + [Theory] + [BitAutoData] + public async Task AcceptOrgUserAsync_UserWithout2FAJoining2FARequiredOrg_ThrowsBadRequest_vNext( + SutProvider sutProvider, + User user, Organization org, OrganizationUser orgUser) + { + // Arrange + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + sutProvider.GetDependency().GetAsync(user.Id).Returns( + SingleOrganizationPolicyRequirement.Create([])); + ArrangeOrgUser(orgUser, user, org); + + // User doesn't have 2FA enabled + _userService.TwoFactorIsEnabledAsync(user).Returns(false); + + // Organization they are trying to join requires 2FA + var twoFactorPolicy = new OrganizationUserPolicyDetails { OrganizationId = orgUser.OrganizationId }; + sutProvider.GetDependency() + .GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication, + OrganizationUserStatusType.Invited) + .Returns(Task.FromResult>( + new List { twoFactorPolicy })); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService)); + + Assert.Equal("You cannot join this organization until you enable two-step login on your user account.", + exception.Message); + } + [Theory] [BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Owner)] @@ -619,10 +714,7 @@ private void SetupCommonAcceptOrgUserMocks(SutProvider sut OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) { // Arrange - orgUser.Email = user.Email; - orgUser.Status = OrganizationUserStatusType.Invited; - orgUser.Type = OrganizationUserType.User; - orgUser.OrganizationId = org.Id; + ArrangeOrgUser(orgUser, user, org); // User is not part of any other orgs sutProvider.GetDependency() @@ -667,6 +759,13 @@ private void SetupCommonAcceptOrgUserMocks(SutProvider sut .Returns(Task.FromResult(org)); } + private static void ArrangeOrgUser(OrganizationUser orgUser, User user, Organization org) + { + orgUser.Email = user.Email; + orgUser.Status = OrganizationUserStatusType.Invited; + orgUser.Type = OrganizationUserType.User; + orgUser.OrganizationId = org.Id; + } private string CreateOldToken(SutProvider sutProvider, OrganizationUser organizationUser) From 377cc823e807d413b52a562416108b59e2db5461 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Fri, 7 Feb 2025 13:28:39 +1000 Subject: [PATCH 12/20] Add to signup command --- .../CloudOrganizationSignUpCommand.cs | 25 +++++++++++-- .../SingleOrganizationPolicyRequirement.cs | 9 +++-- .../Helpers/SutProviderExtensions.cs | 11 ++++++ .../AcceptOrgUserCommandTests.cs | 7 ++-- .../CloudOrganizationSignUpCommandTests.cs | 35 +++++++++++++++++++ 5 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 test/Core.Test/AdminConsole/Helpers/SutProviderExtensions.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs index 94ff3c005922..d40618f75c7d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs @@ -1,5 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.Sales; @@ -45,8 +47,13 @@ public class CloudOrganizationSignUpCommand( IPushRegistrationService pushRegistrationService, IPushNotificationService pushNotificationService, ICollectionRepository collectionRepository, - IDeviceRepository deviceRepository) : ICloudOrganizationSignUpCommand + IDeviceRepository deviceRepository, + IPolicyRequirementQuery policyRequirementQuery) : ICloudOrganizationSignUpCommand { + public const string ErrorBlockedBySingleOrganizationPolicy = + "You may not create an organization. You belong to an organization " + + "which has a policy that prohibits you from being a member of any other organization."; + public async Task SignUpOrganizationAsync(OrganizationSignup signup) { var plan = StaticStore.GetPlan(signup.Plan); @@ -63,10 +70,14 @@ public async Task SignUpOrganizationAsync(Organizati ValidateSecretsManagerPlan(plan, signup); } - if (!signup.IsFromProvider) + if (!signup.IsFromProvider && !featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) { await ValidateSignUpPoliciesAsync(signup.Owner.Id); } + else if (featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) + { + await ValidateSignUpPoliciesAsync_vNext(signup.Owner.Id); + } var organization = new Organization { @@ -258,6 +269,16 @@ private async Task ValidateSignUpPoliciesAsync(Guid ownerId) } } + private async Task ValidateSignUpPoliciesAsync_vNext(Guid ownerId) + { + var singleOrganizationRequirement = + await policyRequirementQuery.GetAsync(ownerId); + if (!singleOrganizationRequirement.CanCreateOrganization()) + { + throw new BadRequestException(ErrorBlockedBySingleOrganizationPolicy); + } + } + private async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignUpAsync(Organization organization, Guid ownerId, string ownerKey, string collectionName, bool withPayment) { diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs index cad652eef952..d22c80f016c6 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs @@ -1,5 +1,4 @@ using Bit.Core.AdminConsole.Enums; -using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; @@ -46,6 +45,10 @@ public SingleOrganizationRequirementResult CanJoinOrganization(Guid organization return SingleOrganizationRequirementResult.Ok; } - public SingleOrganizationRequirementResult CanBeRestoredToOrganization(Guid organizationId) => - CanJoinOrganization(organizationId); + public SingleOrganizationRequirementResult CanBeRestoredToOrganization(Guid organizationId) + => CanJoinOrganization(organizationId); + + public bool CanCreateOrganization() + // Check for other orgs the user might already be a member of (accepted or confirmed status only) + => PolicyDetails.ExcludeRevokedAndInvitedUsers().Any(); } diff --git a/test/Core.Test/AdminConsole/Helpers/SutProviderExtensions.cs b/test/Core.Test/AdminConsole/Helpers/SutProviderExtensions.cs new file mode 100644 index 000000000000..c0dd788ce72f --- /dev/null +++ b/test/Core.Test/AdminConsole/Helpers/SutProviderExtensions.cs @@ -0,0 +1,11 @@ +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using NSubstitute; + +namespace Bit.Core.Test.AdminConsole.Helpers; + +public static class SutProviderExtensions +{ + public static void EnableFeatureFlag(this SutProvider sutProvider, string featureFlag) + => sutProvider.GetDependency().IsEnabled(featureFlag).Returns(true); +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs index 4c1ea4464fa3..2f709d22c92c 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs @@ -13,6 +13,7 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; +using Bit.Core.Test.AdminConsole.Helpers; using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Tokens; using Bit.Core.Utilities; @@ -140,7 +141,7 @@ public async Task AcceptOrgUser_UserJoiningOrgWithSingleOrgPolicyWhileInAnotherO User user, Organization org, OrganizationUser orgUser) { // Arrange - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + sutProvider.EnableFeatureFlag(FeatureFlagKeys.PolicyRequirements); ArrangeOrgUser(orgUser, user, org); // Make organization they are trying to join have the single org policy @@ -194,7 +195,7 @@ public async Task AcceptOrgUserAsync_UserInOrgWithSingleOrgPolicyAlready_ThrowsB User user, Organization org, OrganizationUser orgUser) { // Arrange - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + sutProvider.EnableFeatureFlag(FeatureFlagKeys.PolicyRequirements); ArrangeOrgUser(orgUser, user, org); // Mock that user is part of an org that has the single org policy @@ -254,7 +255,7 @@ public async Task AcceptOrgUserAsync_UserWithout2FAJoining2FARequiredOrg_ThrowsB User user, Organization org, OrganizationUser orgUser) { // Arrange - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + sutProvider.EnableFeatureFlag(FeatureFlagKeys.PolicyRequirements); sutProvider.GetDependency().GetAsync(user.Id).Returns( SingleOrganizationPolicyRequirement.Create([])); ArrangeOrgUser(orgUser, user, org); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs index 859c74f3d0f7..4272e655f63e 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs @@ -1,5 +1,8 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Services; @@ -8,7 +11,9 @@ using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; +using Bit.Core.Test.AdminConsole.Helpers; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Services; @@ -258,4 +263,34 @@ public async Task SignUpAsync_Free_ExistingFreeOrgAdmin_ThrowsBadRequest( () => sutProvider.Sut.SignUpOrganizationAsync(signup)); Assert.Contains("You can only be an admin of one free organization.", exception.Message); } + + [Theory, BitAutoData] + public async Task UserIsBlockedBySingleOrganizationPolicy_ThrowsAsync(OrganizationSignup signup, SutProvider sutProvider) + { + signup.Plan = PlanType.EnterpriseAnnually; + signup.AdditionalSeats = 1; + signup.PaymentMethodType = PaymentMethodType.Card; + signup.PremiumAccessAddon = false; + signup.UseSecretsManager = false; + signup.IsFromProvider = false; + + sutProvider.EnableFeatureFlag(FeatureFlagKeys.PolicyRequirements); + var singleOrganizationRequirement = SingleOrganizationPolicyRequirement.Create([ + new OrganizationUserPolicyDetails + { + OrganizationId = Guid.NewGuid(), + OrganizationUserId = Guid.NewGuid(), + OrganizationUserStatus = OrganizationUserStatusType.Confirmed, + PolicyType = PolicyType.SingleOrg, + PolicyEnabled = true + } + ]); + sutProvider.GetDependency() + .GetAsync(signup.Owner.Id) + .Returns(singleOrganizationRequirement); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpOrganizationAsync(signup)); + Assert.Equal(CloudOrganizationSignUpCommand.ErrorBlockedBySingleOrganizationPolicy, exception.Message); + } } From e479c1d20a3ab170058b525e02652c62fced64d3 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Fri, 7 Feb 2025 13:40:17 +1000 Subject: [PATCH 13/20] Move error strings to requirement class --- .../OrganizationUsers/AcceptOrgUserCommand.cs | 7 ++----- .../Organizations/CloudOrganizationSignUpCommand.cs | 5 +---- .../SingleOrganizationPolicyRequirement.cs | 6 ++++++ .../OrganizationUsers/AcceptOrgUserCommandTests.cs | 6 ++++-- .../CloudOrganizationSignUpCommandTests.cs | 2 +- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs index 136a4ab020cd..b9366a6e9fb7 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs @@ -56,9 +56,6 @@ public AcceptOrgUserCommand( _featureService = featureService; } - public const string ErrorBlockedByThisSingleOrganizationPolicy = "You may not join this organization until you leave or remove all other organizations."; - public const string ErrorBlockedByOtherSingleOrganizationPolicy = "You cannot join this organization because you are a member of another organization which forbids it"; - public async Task AcceptOrgUserByEmailTokenAsync(Guid organizationUserId, User user, string emailToken, IUserService userService) { @@ -250,9 +247,9 @@ private async Task EnforcePolicies_vNext(User user, OrganizationUser orgUser, IU switch (singleOrganizationRequirement.CanJoinOrganization(orgUser.OrganizationId)) { case SingleOrganizationRequirementResult.BlockedByThisOrganization: - throw new BadRequestException(ErrorBlockedByThisSingleOrganizationPolicy); + throw new BadRequestException(SingleOrganizationPolicyRequirement.ErrorCannotJoinBlockedByThisOrganization); case SingleOrganizationRequirementResult.BlockedByOtherOrganization: - throw new BadRequestException(ErrorBlockedByOtherSingleOrganizationPolicy); + throw new BadRequestException(SingleOrganizationPolicyRequirement.ErrorCannotJoinBlockedByOtherOrganization); case SingleOrganizationRequirementResult.Ok: default: break; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs index d40618f75c7d..6e85d308041d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs @@ -50,9 +50,6 @@ public class CloudOrganizationSignUpCommand( IDeviceRepository deviceRepository, IPolicyRequirementQuery policyRequirementQuery) : ICloudOrganizationSignUpCommand { - public const string ErrorBlockedBySingleOrganizationPolicy = - "You may not create an organization. You belong to an organization " + - "which has a policy that prohibits you from being a member of any other organization."; public async Task SignUpOrganizationAsync(OrganizationSignup signup) { @@ -275,7 +272,7 @@ private async Task ValidateSignUpPoliciesAsync_vNext(Guid ownerId) await policyRequirementQuery.GetAsync(ownerId); if (!singleOrganizationRequirement.CanCreateOrganization()) { - throw new BadRequestException(ErrorBlockedBySingleOrganizationPolicy); + throw new BadRequestException(SingleOrganizationPolicyRequirement.ErrorCannotCreateOrganization); } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs index d22c80f016c6..77b03cd8bda3 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs @@ -12,6 +12,12 @@ public enum SingleOrganizationRequirementResult public class SingleOrganizationPolicyRequirement : IPolicyRequirement { + public const string ErrorCannotJoinBlockedByThisOrganization = "You may not join this organization until you leave or remove all other organizations."; + public const string ErrorCannotJoinBlockedByOtherOrganization = "You cannot join this organization because you are a member of another organization which forbids it"; + public const string ErrorCannotCreateOrganization = + "You may not create an organization. You belong to an organization " + + "which has a policy that prohibits you from being a member of any other organization."; + /// /// Single organization policy details filtered by user role but not status. /// This lets us check whether a user is compliant before being accepted/restored. diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs index 2f709d22c92c..b56a503cf98a 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs @@ -162,7 +162,8 @@ public async Task AcceptOrgUser_UserJoiningOrgWithSingleOrgPolicyWhileInAnotherO var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService)); - Assert.Equal(AcceptOrgUserCommand.ErrorBlockedByThisSingleOrganizationPolicy, exception.Message); + Assert.Equal(SingleOrganizationPolicyRequirement.ErrorCannotJoinBlockedByThisOrganization, + exception.Message); } [Theory] @@ -217,7 +218,8 @@ public async Task AcceptOrgUserAsync_UserInOrgWithSingleOrgPolicyAlready_ThrowsB var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService)); - Assert.Equal(AcceptOrgUserCommand.ErrorBlockedByOtherSingleOrganizationPolicy, exception.Message); + Assert.Equal(SingleOrganizationPolicyRequirement.ErrorCannotJoinBlockedByOtherOrganization, + exception.Message); } [Theory] diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs index 4272e655f63e..a5e99ea9aa12 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs @@ -291,6 +291,6 @@ public async Task UserIsBlockedBySingleOrganizationPolicy_ThrowsAsync(Organizati var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.SignUpOrganizationAsync(signup)); - Assert.Equal(CloudOrganizationSignUpCommand.ErrorBlockedBySingleOrganizationPolicy, exception.Message); + Assert.Equal(SingleOrganizationPolicyRequirement.ErrorCannotCreateOrganization, exception.Message); } } From 09687744b9fe106e34b76d6f151bc13d58ef9af0 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 11 Feb 2025 09:57:24 +1000 Subject: [PATCH 14/20] revert singleorg req --- .../OrganizationUsers/AcceptOrgUserCommand.cs | 79 +++--------- .../CloudOrganizationSignUpCommand.cs | 22 +--- .../Implementations/PolicyRequirementQuery.cs | 2 - .../SingleOrganizationPolicyRequirement.cs | 60 ---------- .../AcceptOrgUserCommandTests.cs | 112 +----------------- .../CloudOrganizationSignUpCommandTests.cs | 35 ------ 6 files changed, 23 insertions(+), 287 deletions(-) delete mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs index b9366a6e9fb7..756bd2ae46d7 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs @@ -1,6 +1,4 @@ using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Billing.Enums; @@ -15,7 +13,7 @@ using Bit.Core.Utilities; using Microsoft.AspNetCore.DataProtection; -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +namespace Bit.Core.OrganizationFeatures.OrganizationUsers; public class AcceptOrgUserCommand : IAcceptOrgUserCommand { @@ -27,8 +25,6 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand private readonly IMailService _mailService; private readonly IUserRepository _userRepository; private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; - private readonly IPolicyRequirementQuery _policyRequirementQuery; - private readonly IFeatureService _featureService; public AcceptOrgUserCommand( IDataProtectionProvider dataProtectionProvider, @@ -38,9 +34,7 @@ public AcceptOrgUserCommand( IPolicyService policyService, IMailService mailService, IUserRepository userRepository, - IDataProtectorTokenFactory orgUserInviteTokenDataFactory, - IPolicyRequirementQuery policyRequirementQuery, - IFeatureService featureService) + IDataProtectorTokenFactory orgUserInviteTokenDataFactory) { // TODO: remove data protector when old token validation removed @@ -52,8 +46,6 @@ public AcceptOrgUserCommand( _mailService = mailService; _userRepository = userRepository; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; - _policyRequirementQuery = policyRequirementQuery; - _featureService = featureService; } public async Task AcceptOrgUserByEmailTokenAsync(Guid organizationUserId, User user, string emailToken, @@ -180,35 +172,7 @@ public async Task AcceptOrgUserAsync(OrganizationUser orgUser, } } - if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) - { - await EnforcePolicies_vNext(user, orgUser, userService); - } - else - { - await EnforcePolicies(user, orgUser, userService); - } - - orgUser.Status = OrganizationUserStatusType.Accepted; - orgUser.UserId = user.Id; - orgUser.Email = null; - - await _organizationUserRepository.ReplaceAsync(orgUser); - - var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(orgUser.OrganizationId, OrganizationUserType.Admin); - var adminEmails = admins.Select(a => a.Email).Distinct().ToList(); - - if (adminEmails.Count > 0) - { - var organization = await _organizationRepository.GetByIdAsync(orgUser.OrganizationId); - await _mailService.SendOrganizationAcceptedEmailAsync(organization, user.Email, adminEmails); - } - - return orgUser; - } - - private async Task EnforcePolicies(User user, OrganizationUser orgUser, IUserService userService) - { + // Enforce Single Organization Policy of organization user is trying to join var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(user.Id); var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId); var invitedSingleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, @@ -237,34 +201,23 @@ private async Task EnforcePolicies(User user, OrganizationUser orgUser, IUserSer throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account."); } } - } - private async Task EnforcePolicies_vNext(User user, OrganizationUser orgUser, IUserService userService) - { - var singleOrganizationRequirement = - await _policyRequirementQuery.GetAsync(user.Id); + orgUser.Status = OrganizationUserStatusType.Accepted; + orgUser.UserId = user.Id; + orgUser.Email = null; - switch (singleOrganizationRequirement.CanJoinOrganization(orgUser.OrganizationId)) - { - case SingleOrganizationRequirementResult.BlockedByThisOrganization: - throw new BadRequestException(SingleOrganizationPolicyRequirement.ErrorCannotJoinBlockedByThisOrganization); - case SingleOrganizationRequirementResult.BlockedByOtherOrganization: - throw new BadRequestException(SingleOrganizationPolicyRequirement.ErrorCannotJoinBlockedByOtherOrganization); - case SingleOrganizationRequirementResult.Ok: - default: - break; - } + await _organizationUserRepository.ReplaceAsync(orgUser); - // TODO: this will be rewritten once a requirement is added - // Enforce Two Factor Authentication Policy of organization user is trying to join - if (!await userService.TwoFactorIsEnabledAsync(user)) + var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(orgUser.OrganizationId, OrganizationUserType.Admin); + var adminEmails = admins.Select(a => a.Email).Distinct().ToList(); + + if (adminEmails.Count > 0) { - var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, - PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); - if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) - { - throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account."); - } + var organization = await _organizationRepository.GetByIdAsync(orgUser.OrganizationId); + await _mailService.SendOrganizationAcceptedEmailAsync(organization, user.Email, adminEmails); } + + return orgUser; } + } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs index 6e85d308041d..94ff3c005922 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs @@ -1,7 +1,5 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.Sales; @@ -47,10 +45,8 @@ public class CloudOrganizationSignUpCommand( IPushRegistrationService pushRegistrationService, IPushNotificationService pushNotificationService, ICollectionRepository collectionRepository, - IDeviceRepository deviceRepository, - IPolicyRequirementQuery policyRequirementQuery) : ICloudOrganizationSignUpCommand + IDeviceRepository deviceRepository) : ICloudOrganizationSignUpCommand { - public async Task SignUpOrganizationAsync(OrganizationSignup signup) { var plan = StaticStore.GetPlan(signup.Plan); @@ -67,14 +63,10 @@ public async Task SignUpOrganizationAsync(Organizati ValidateSecretsManagerPlan(plan, signup); } - if (!signup.IsFromProvider && !featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) + if (!signup.IsFromProvider) { await ValidateSignUpPoliciesAsync(signup.Owner.Id); } - else if (featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) - { - await ValidateSignUpPoliciesAsync_vNext(signup.Owner.Id); - } var organization = new Organization { @@ -266,16 +258,6 @@ private async Task ValidateSignUpPoliciesAsync(Guid ownerId) } } - private async Task ValidateSignUpPoliciesAsync_vNext(Guid ownerId) - { - var singleOrganizationRequirement = - await policyRequirementQuery.GetAsync(ownerId); - if (!singleOrganizationRequirement.CanCreateOrganization()) - { - throw new BadRequestException(SingleOrganizationPolicyRequirement.ErrorCannotCreateOrganization); - } - } - private async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignUpAsync(Organization organization, Guid ownerId, string ownerKey, string collectionName, bool withPayment) { diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs index 4914024a68ab..73116ce6df32 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs @@ -1,7 +1,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Models.Data.Organizations.OrganizationUsers; -using Bit.Core.Settings; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; @@ -15,7 +14,6 @@ public PolicyRequirementQuery(IPolicyRepository policyRepository) _policyRepository = policyRepository; // Register Policy Requirement factory functions below - _policyRequirements.Add(SingleOrganizationPolicyRequirement.Create); } public async Task GetAsync(Guid userId) where T : IPolicyRequirement diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs deleted file mode 100644 index 77b03cd8bda3..000000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Bit.Core.AdminConsole.Enums; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; - -namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; - -public enum SingleOrganizationRequirementResult -{ - Ok = 1, - BlockedByThisOrganization = 2, - BlockedByOtherOrganization = 3 -} - -public class SingleOrganizationPolicyRequirement : IPolicyRequirement -{ - public const string ErrorCannotJoinBlockedByThisOrganization = "You may not join this organization until you leave or remove all other organizations."; - public const string ErrorCannotJoinBlockedByOtherOrganization = "You cannot join this organization because you are a member of another organization which forbids it"; - public const string ErrorCannotCreateOrganization = - "You may not create an organization. You belong to an organization " + - "which has a policy that prohibits you from being a member of any other organization."; - - /// - /// Single organization policy details filtered by user role but not status. - /// This lets us check whether a user is compliant before being accepted/restored. - /// - private IEnumerable PolicyDetails { get; init; } - - public static SingleOrganizationPolicyRequirement Create(IEnumerable userPolicyDetails) - => new() - { - PolicyDetails = userPolicyDetails - .GetPolicyType(PolicyType.SingleOrg) - .ExcludeOwnersAndAdmins() - .ExcludeProviders() - .ToList() - }; - - public SingleOrganizationRequirementResult CanJoinOrganization(Guid organizationId) - { - // Check for the org the user is trying to join; status doesn't matter - if (PolicyDetails.Any(x => x.OrganizationId == organizationId)) - { - return SingleOrganizationRequirementResult.BlockedByThisOrganization; - } - - // Check for other orgs the user might already be a member of (accepted or confirmed status only) - if (PolicyDetails.ExcludeRevokedAndInvitedUsers().Any()) - { - return SingleOrganizationRequirementResult.BlockedByOtherOrganization; - } - - return SingleOrganizationRequirementResult.Ok; - } - - public SingleOrganizationRequirementResult CanBeRestoredToOrganization(Guid organizationId) - => CanJoinOrganization(organizationId); - - public bool CanCreateOrganization() - // Check for other orgs the user might already be a member of (accepted or confirmed status only) - => PolicyDetails.ExcludeRevokedAndInvitedUsers().Any(); -} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs index b56a503cf98a..2dda23481a81 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs @@ -1,8 +1,5 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Billing.Enums; @@ -10,10 +7,10 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.OrganizationFeatures.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Test.AdminConsole.Helpers; using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Tokens; using Bit.Core.Utilities; @@ -134,38 +131,6 @@ public async Task AcceptOrgUser_UserJoiningOrgWithSingleOrgPolicyWhileInAnotherO exception.Message); } - [Theory] - [BitAutoData] - public async Task AcceptOrgUser_UserJoiningOrgWithSingleOrgPolicyWhileInAnotherOrg_ThrowsBadRequest_vNext( - SutProvider sutProvider, - User user, Organization org, OrganizationUser orgUser) - { - // Arrange - sutProvider.EnableFeatureFlag(FeatureFlagKeys.PolicyRequirements); - ArrangeOrgUser(orgUser, user, org); - - // Make organization they are trying to join have the single org policy - var singleOrganizationRequirement = SingleOrganizationPolicyRequirement.Create([ - new OrganizationUserPolicyDetails - { - OrganizationId = org.Id, - OrganizationUserStatus = OrganizationUserStatusType.Invited, - OrganizationUserType = OrganizationUserType.User, - PolicyEnabled = true, - PolicyType = PolicyType.SingleOrg - } - ]); - sutProvider.GetDependency().GetAsync(user.Id) - .Returns(singleOrganizationRequirement); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService)); - - Assert.Equal(SingleOrganizationPolicyRequirement.ErrorCannotJoinBlockedByThisOrganization, - exception.Message); - } - [Theory] [BitAutoData] public async Task AcceptOrgUserAsync_UserInOrgWithSingleOrgPolicyAlready_ThrowsBadRequest( @@ -189,38 +154,6 @@ public async Task AcceptOrgUserAsync_UserInOrgWithSingleOrgPolicyAlready_ThrowsB exception.Message); } - [Theory] - [BitAutoData] - public async Task AcceptOrgUserAsync_UserInOrgWithSingleOrgPolicyAlready_ThrowsBadRequest_vNext( - SutProvider sutProvider, - User user, Organization org, OrganizationUser orgUser) - { - // Arrange - sutProvider.EnableFeatureFlag(FeatureFlagKeys.PolicyRequirements); - ArrangeOrgUser(orgUser, user, org); - - // Mock that user is part of an org that has the single org policy - var singleOrganizationRequirement = SingleOrganizationPolicyRequirement.Create([ - new OrganizationUserPolicyDetails - { - OrganizationId = Guid.NewGuid(), - OrganizationUserId = Guid.NewGuid(), - OrganizationUserStatus = OrganizationUserStatusType.Accepted, - OrganizationUserType = OrganizationUserType.User, - PolicyEnabled = true, - PolicyType = PolicyType.SingleOrg - } - ]); - sutProvider.GetDependency().GetAsync(user.Id) - .Returns(singleOrganizationRequirement); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService)); - - Assert.Equal(SingleOrganizationPolicyRequirement.ErrorCannotJoinBlockedByOtherOrganization, - exception.Message); - } [Theory] [BitAutoData] @@ -250,37 +183,6 @@ public async Task AcceptOrgUserAsync_UserWithout2FAJoining2FARequiredOrg_ThrowsB exception.Message); } - [Theory] - [BitAutoData] - public async Task AcceptOrgUserAsync_UserWithout2FAJoining2FARequiredOrg_ThrowsBadRequest_vNext( - SutProvider sutProvider, - User user, Organization org, OrganizationUser orgUser) - { - // Arrange - sutProvider.EnableFeatureFlag(FeatureFlagKeys.PolicyRequirements); - sutProvider.GetDependency().GetAsync(user.Id).Returns( - SingleOrganizationPolicyRequirement.Create([])); - ArrangeOrgUser(orgUser, user, org); - - // User doesn't have 2FA enabled - _userService.TwoFactorIsEnabledAsync(user).Returns(false); - - // Organization they are trying to join requires 2FA - var twoFactorPolicy = new OrganizationUserPolicyDetails { OrganizationId = orgUser.OrganizationId }; - sutProvider.GetDependency() - .GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication, - OrganizationUserStatusType.Invited) - .Returns(Task.FromResult>( - new List { twoFactorPolicy })); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService)); - - Assert.Equal("You cannot join this organization until you enable two-step login on your user account.", - exception.Message); - } - [Theory] [BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Owner)] @@ -717,7 +619,10 @@ private void SetupCommonAcceptOrgUserMocks(SutProvider sut OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) { // Arrange - ArrangeOrgUser(orgUser, user, org); + orgUser.Email = user.Email; + orgUser.Status = OrganizationUserStatusType.Invited; + orgUser.Type = OrganizationUserType.User; + orgUser.OrganizationId = org.Id; // User is not part of any other orgs sutProvider.GetDependency() @@ -762,13 +667,6 @@ private void SetupCommonAcceptOrgUserMocks(SutProvider sut .Returns(Task.FromResult(org)); } - private static void ArrangeOrgUser(OrganizationUser orgUser, User user, Organization org) - { - orgUser.Email = user.Email; - orgUser.Status = OrganizationUserStatusType.Invited; - orgUser.Type = OrganizationUserType.User; - orgUser.OrganizationId = org.Id; - } private string CreateOldToken(SutProvider sutProvider, OrganizationUser organizationUser) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs index a5e99ea9aa12..859c74f3d0f7 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs @@ -1,8 +1,5 @@ using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Services; @@ -11,9 +8,7 @@ using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.Data; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; -using Bit.Core.Test.AdminConsole.Helpers; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Services; @@ -263,34 +258,4 @@ public async Task SignUpAsync_Free_ExistingFreeOrgAdmin_ThrowsBadRequest( () => sutProvider.Sut.SignUpOrganizationAsync(signup)); Assert.Contains("You can only be an admin of one free organization.", exception.Message); } - - [Theory, BitAutoData] - public async Task UserIsBlockedBySingleOrganizationPolicy_ThrowsAsync(OrganizationSignup signup, SutProvider sutProvider) - { - signup.Plan = PlanType.EnterpriseAnnually; - signup.AdditionalSeats = 1; - signup.PaymentMethodType = PaymentMethodType.Card; - signup.PremiumAccessAddon = false; - signup.UseSecretsManager = false; - signup.IsFromProvider = false; - - sutProvider.EnableFeatureFlag(FeatureFlagKeys.PolicyRequirements); - var singleOrganizationRequirement = SingleOrganizationPolicyRequirement.Create([ - new OrganizationUserPolicyDetails - { - OrganizationId = Guid.NewGuid(), - OrganizationUserId = Guid.NewGuid(), - OrganizationUserStatus = OrganizationUserStatusType.Confirmed, - PolicyType = PolicyType.SingleOrg, - PolicyEnabled = true - } - ]); - sutProvider.GetDependency() - .GetAsync(signup.Owner.Id) - .Returns(singleOrganizationRequirement); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.SignUpOrganizationAsync(signup)); - Assert.Equal(SingleOrganizationPolicyRequirement.ErrorCannotCreateOrganization, exception.Message); - } } From f75636687146641a61aad67cb1232ad183771a9e Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 11 Feb 2025 09:58:57 +1000 Subject: [PATCH 15/20] remove unused helper --- .../AdminConsole/Helpers/SutProviderExtensions.cs | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 test/Core.Test/AdminConsole/Helpers/SutProviderExtensions.cs diff --git a/test/Core.Test/AdminConsole/Helpers/SutProviderExtensions.cs b/test/Core.Test/AdminConsole/Helpers/SutProviderExtensions.cs deleted file mode 100644 index c0dd788ce72f..000000000000 --- a/test/Core.Test/AdminConsole/Helpers/SutProviderExtensions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Bit.Core.Services; -using Bit.Test.Common.AutoFixture; -using NSubstitute; - -namespace Bit.Core.Test.AdminConsole.Helpers; - -public static class SutProviderExtensions -{ - public static void EnableFeatureFlag(this SutProvider sutProvider, string featureFlag) - => sutProvider.GetDependency().IsEnabled(featureFlag).Returns(true); -} From db4bed4a52836c70660b08982e2f3aa05146f3b0 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 11 Feb 2025 10:30:20 +1000 Subject: [PATCH 16/20] Use new data class --- .../OrganizationUserPolicyDetails.cs | 9 +---- .../Organizations/Policies/PolicyDetails.cs | 38 +++++++++++++++++++ .../Implementations/PolicyRequirementQuery.cs | 10 ++--- .../PolicyRequirements/IPolicyRequirement.cs | 4 +- .../Repositories/IPolicyRepository.cs | 4 +- .../AdminConsole/Services/IPolicyService.cs | 2 + .../Services/Implementations/UserService.cs | 2 +- .../Repositories/PolicyRepository.cs | 6 +-- .../Repositories/PolicyRepository.cs | 4 +- 9 files changed, 56 insertions(+), 23 deletions(-) create mode 100644 src/Core/AdminConsole/Models/Data/Organizations/Policies/PolicyDetails.cs diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs index 584ca02a6f24..84939ecf791e 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs @@ -1,13 +1,11 @@ using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.Enums; -using Bit.Core.Utilities; namespace Bit.Core.Models.Data.Organizations.OrganizationUsers; public class OrganizationUserPolicyDetails { - public Guid? OrganizationUserId { get; set; } + public Guid OrganizationUserId { get; set; } public Guid OrganizationId { get; set; } @@ -24,9 +22,4 @@ public class OrganizationUserPolicyDetails public string OrganizationUserPermissionsData { get; set; } public bool IsProvider { get; set; } - - public T GetDataModel() where T : IPolicyDataModel, new() - { - return CoreHelpers.LoadClassFromJsonData(PolicyData); - } } diff --git a/src/Core/AdminConsole/Models/Data/Organizations/Policies/PolicyDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/Policies/PolicyDetails.cs new file mode 100644 index 000000000000..5b87c18824cb --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/Organizations/Policies/PolicyDetails.cs @@ -0,0 +1,38 @@ +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; + +/// +/// 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 to create requirements for specific policy types. +/// +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; } + public string OrganizationUserCustomPermissions { get; set; } + + /// + /// True if the user is also a ProviderUser for the organization, false otherwise. + /// + public bool IsProvider { get; set; } + + public T GetDataModel() where T : IPolicyDataModel, new() + => CoreHelpers.LoadClassFromJsonData(PolicyData); + + public Permissions GetOrganizationUserCustomPermissions + => CoreHelpers.LoadClassFromJsonData(OrganizationUserCustomPermissions); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs index 73116ce6df32..536e810c1e3a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs @@ -1,6 +1,6 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; @@ -19,7 +19,7 @@ public PolicyRequirementQuery(IPolicyRepository policyRepository) public async Task GetAsync(Guid userId) where T : IPolicyRequirement => _policyRequirements.Get()(await GetPolicyDetails(userId)); - private Task> GetPolicyDetails(Guid userId) => + private Task> GetPolicyDetails(Guid userId) => _policyRepository.GetPolicyDetailsByUserId(userId); /// @@ -32,7 +32,7 @@ private class PolicyRequirementRegistry public void Add(CreateRequirement factory) where T : IPolicyRequirement { // Explicitly convert T to an IPolicyRequirement (C# doesn't do this automatically). - IPolicyRequirement Converted(IEnumerable up) => factory(up); + IPolicyRequirement Converted(IEnumerable p) => factory(p); _registry.Add(typeof(T), Converted); } @@ -45,7 +45,7 @@ public CreateRequirement Get() where T : IPolicyRequirement // Explicitly convert IPolicyRequirement back to T (C# doesn't do this automatically). // The cast here relies on the Register method correctly associating the type and factory function. - T Converted(IEnumerable up) => (T)factory(up); + T Converted(IEnumerable p) => (T)factory(p); return Converted; } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs index afec98cc4079..19fce04b98fb 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs @@ -1,8 +1,8 @@ -using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; public interface IPolicyRequirement; -public delegate T CreateRequirement(IEnumerable userPolicyDetails) +public delegate T CreateRequirement(IEnumerable policyDetails) where T : IPolicyRequirement; diff --git a/src/Core/AdminConsole/Repositories/IPolicyRepository.cs b/src/Core/AdminConsole/Repositories/IPolicyRepository.cs index 62a72ec3d725..2ad95101b2f4 100644 --- a/src/Core/AdminConsole/Repositories/IPolicyRepository.cs +++ b/src/Core/AdminConsole/Repositories/IPolicyRepository.cs @@ -1,6 +1,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.Repositories; #nullable enable @@ -12,5 +12,5 @@ public interface IPolicyRepository : IRepository Task GetByOrganizationIdTypeAsync(Guid organizationId, PolicyType type); Task> GetManyByOrganizationIdAsync(Guid organizationId); Task> GetManyByUserIdAsync(Guid userId); - Task> GetPolicyDetailsByUserId(Guid userId); + Task> GetPolicyDetailsByUserId(Guid userId); } diff --git a/src/Core/AdminConsole/Services/IPolicyService.cs b/src/Core/AdminConsole/Services/IPolicyService.cs index 4f9a25f90455..f1ac681d6ff3 100644 --- a/src/Core/AdminConsole/Services/IPolicyService.cs +++ b/src/Core/AdminConsole/Services/IPolicyService.cs @@ -12,6 +12,8 @@ public interface IPolicyService /// Get the combined master password policy options for the specified user. /// Task GetMasterPasswordPolicyForUserAsync(User user); + [Obsolete("Use IPolicyRequirementQuery.GetAsync instead. You may have to add a new IPolicyRequirement for that query to return.")] Task> GetPoliciesApplicableToUserAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted); + [Obsolete("Use IPolicyRequirementQuery.GetAsync instead. You may have to add a new IPolicyRequirement for that query to return.")] Task AnyPoliciesApplicableToUserAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted); } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index bafb2c640992..11d4042defb8 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1372,7 +1372,7 @@ private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user) await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync( new RevokeOrganizationUsersRequest( p.OrganizationId, - [new OrganizationUserUserDetails { Id = p.OrganizationUserId!.Value, OrganizationId = p.OrganizationId }], + [new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }], new SystemUser(EventSystemUser.TwoFactorDisabled))); await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email); } diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs index 2d136cb875b4..071ff3153a4e 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs @@ -1,8 +1,8 @@ using System.Data; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Settings; using Bit.Infrastructure.Dapper.Repositories; using Dapper; @@ -61,11 +61,11 @@ public async Task> GetManyByUserIdAsync(Guid userId) } } - public async Task> GetPolicyDetailsByUserId(Guid userId) + public async Task> GetPolicyDetailsByUserId(Guid userId) { using (var connection = new SqlConnection(ConnectionString)) { - var results = await connection.QueryAsync( + var results = await connection.QueryAsync( $"[{Schema}].[PolicyDetails_ReadByUserId]", new { UserId = userId }, commandType: CommandType.StoredProcedure); diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs index a7e6f2ce0ba2..6aa3e9f70ec3 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs @@ -1,7 +1,7 @@ using AutoMapper; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries; using Bit.Infrastructure.EntityFramework.Repositories; @@ -52,5 +52,5 @@ public PolicyRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper } } - public Task> GetPolicyDetailsByUserId(Guid userId) => throw new NotImplementedException(); + public Task> GetPolicyDetailsByUserId(Guid userId) => throw new NotImplementedException(); } From 6d7330b74f6a7b91e7c318ce2b78654099b3e540 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 11 Feb 2025 11:57:25 +1000 Subject: [PATCH 17/20] Add EF implementation and tests --- .../Organizations/Policies/PolicyDetails.cs | 10 +- .../Repositories/PolicyRepository.cs | 40 ++- .../AdminConsole/OrganizationTestHelpers.cs | 58 ++++ .../GetPolicyDetailsByUserIdTests.cs | 302 ++++++++++++++++++ ...25-02-11_00_PolicyDetails_ReadByUserId.sql | 37 +++ 5 files changed, 443 insertions(+), 4 deletions(-) create mode 100644 test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs create mode 100644 test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs create mode 100644 util/Migrator/DbScripts/2025-02-11_00_PolicyDetails_ReadByUserId.sql diff --git a/src/Core/AdminConsole/Models/Data/Organizations/Policies/PolicyDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/Policies/PolicyDetails.cs index 5b87c18824cb..d8b191822fe2 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/Policies/PolicyDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/Policies/PolicyDetails.cs @@ -23,7 +23,11 @@ public class PolicyDetails public OrganizationUserType OrganizationUserType { get; set; } public OrganizationUserStatusType OrganizationUserStatus { get; set; } - public string OrganizationUserCustomPermissions { get; set; } + /// + /// Custom permissions for the organization user, if any. Use + /// to deserialize. + /// + public string OrganizationUserPermissionsData { get; set; } /// /// True if the user is also a ProviderUser for the organization, false otherwise. @@ -33,6 +37,6 @@ public class PolicyDetails public T GetDataModel() where T : IPolicyDataModel, new() => CoreHelpers.LoadClassFromJsonData(PolicyData); - public Permissions GetOrganizationUserCustomPermissions - => CoreHelpers.LoadClassFromJsonData(OrganizationUserCustomPermissions); + public Permissions GetOrganizationUserCustomPermissions() + => CoreHelpers.LoadClassFromJsonData(OrganizationUserPermissionsData); } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs index 6aa3e9f70ec3..05646813410f 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs @@ -2,6 +2,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Enums; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries; using Bit.Infrastructure.EntityFramework.Repositories; @@ -52,5 +53,42 @@ public PolicyRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper } } - public Task> GetPolicyDetailsByUserId(Guid userId) => throw new NotImplementedException(); + public async Task> 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(); + } } diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs b/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs new file mode 100644 index 000000000000..56beabd47027 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs @@ -0,0 +1,58 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole; + +/// +/// A set of extension methods used to arrange simple test data. +/// This should only be used for basic, repetitive data arrangement, not for anything complex or for +/// the repository method under test. +/// +public static class OrganizationTestHelpers +{ + public static Task CreateTestUserAsync(this IUserRepository userRepository, string identifier = "test") + { + var id = Guid.NewGuid(); + return userRepository.CreateAsync(new User + { + Id = id, + Name = $"{identifier}-{id}", + Email = $"{id}@example.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + } + + public static Task CreateTestOrganizationAsync(this IOrganizationRepository organizationRepository, + string identifier = "test") + => organizationRepository.CreateAsync(new Organization + { + Name = $"{identifier}-{Guid.NewGuid()}", + BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL + Plan = "Test", // TODO: EF does not enforce this being NOT NULl + }); + + public static Task CreateTestOrganizationUserAsync( + this IOrganizationUserRepository organizationUserRepository, + Organization organization, + User user, + OrganizationUserType role = OrganizationUserType.Owner) + => organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = role + }); + + public static Task CreateTestGroupAsync( + this IGroupRepository groupRepository, + Organization organization, + string identifier = "test") + => groupRepository.CreateAsync( + new Group { OrganizationId = organization.Id, Name = $"{identifier} {Guid.NewGuid()}" } + ); +} diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs new file mode 100644 index 000000000000..c365f30e5ce7 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs @@ -0,0 +1,302 @@ +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 + var user = await userRepository.CreateTestUserAsync(); + var org1 = await CreateEnterpriseOrg(organizationRepository); + var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(org1, user, role: OrganizationUserType.Owner); + var policy1 = new Policy + { + OrganizationId = org1.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + Data = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = true, IntSetting = 5 }) + }; + await policyRepository.CreateAsync(policy1); + + // OrgUser2 - custom user of org2 + var org2 = await CreateEnterpriseOrg(organizationRepository); + var orgUser2 = new OrganizationUser + { + OrganizationId = org2.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Custom, + }; + orgUser2.SetPermissions(new Permissions + { + ManagePolicies = true + }); + await organizationUserRepository.CreateAsync(orgUser2); + + var policy2 = new Policy + { + OrganizationId = org2.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + Data = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = false, IntSetting = 15 }) + }; + await policyRepository.CreateAsync(policy2); + + // 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(), 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.Confirmed, + OrganizationUserPermissionsData = CoreHelpers.ClassToJsonData(new Permissions { ManagePolicies = true }), + IsProvider = false + }; + Assert.Equivalent(expectedPolicyDetails2, actualPolicyDetails2); + Assert.Equivalent(expectedPolicyDetails2.GetDataModel(), new TestPolicyData { BoolSetting = false, IntSetting = 15 }); + Assert.Equivalent(new Permissions { ManagePolicies = true }, actualPolicyDetails2.GetOrganizationUserCustomPermissions(), strict: true); + } + + [DatabaseTheory, DatabaseData] + public async Task GetPolicyDetailsByUserId_InvitedUsers_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); + + var policy = new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }; + await policyRepository.CreateAsync(policy); + + // 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_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 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 + }); +} diff --git a/util/Migrator/DbScripts/2025-02-11_00_PolicyDetails_ReadByUserId.sql b/util/Migrator/DbScripts/2025-02-11_00_PolicyDetails_ReadByUserId.sql new file mode 100644 index 000000000000..24154230be74 --- /dev/null +++ b/util/Migrator/DbScripts/2025-02-11_00_PolicyDetails_ReadByUserId.sql @@ -0,0 +1,37 @@ +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 ( + (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 + ) + ) +END From 473341e808c8c352e93128b00db5897b37db789f Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 11 Feb 2025 12:00:18 +1000 Subject: [PATCH 18/20] Tweak tests --- .../GetPolicyDetailsByUserIdTests.cs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs index c365f30e5ce7..b2226a19a05a 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs @@ -28,14 +28,13 @@ public async Task GetPolicyDetailsByUserId_NonInvitedUsers_Works( var user = await userRepository.CreateTestUserAsync(); var org1 = await CreateEnterpriseOrg(organizationRepository); var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(org1, user, role: OrganizationUserType.Owner); - var policy1 = new Policy + await policyRepository.CreateAsync(new Policy { OrganizationId = org1.Id, Enabled = true, Type = PolicyType.SingleOrg, Data = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = true, IntSetting = 5 }) - }; - await policyRepository.CreateAsync(policy1); + }); // OrgUser2 - custom user of org2 var org2 = await CreateEnterpriseOrg(organizationRepository); @@ -51,15 +50,13 @@ public async Task GetPolicyDetailsByUserId_NonInvitedUsers_Works( ManagePolicies = true }); await organizationUserRepository.CreateAsync(orgUser2); - - var policy2 = new Policy + await policyRepository.CreateAsync(new Policy { OrganizationId = org2.Id, Enabled = true, Type = PolicyType.SingleOrg, Data = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = false, IntSetting = 15 }) - }; - await policyRepository.CreateAsync(policy2); + }); // Act var policyDetails = (await policyRepository.GetPolicyDetailsByUserId(user.Id)).ToList(); @@ -118,14 +115,12 @@ public async Task GetPolicyDetailsByUserId_InvitedUsers_Works( Email = user.Email // invited users have matching Email }; await organizationUserRepository.CreateAsync(orgUser); - - var policy = new Policy + await policyRepository.CreateAsync(new Policy { OrganizationId = org.Id, Enabled = true, Type = PolicyType.SingleOrg, - }; - await policyRepository.CreateAsync(policy); + }); // Act var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); From 88120849ca7e8a611d57d488225d0a3f6695322d Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 11 Feb 2025 12:12:55 +1000 Subject: [PATCH 19/20] Enable nullable, add xmldoc --- .../Policies/IPolicyRequirementQuery.cs | 4 +++- .../Implementations/PolicyRequirementQuery.cs | 4 +++- .../PolicyRequirements/IPolicyRequirement.cs | 17 ++++++++++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs index 35821dd9d1f5..63c2946863a0 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs @@ -1,4 +1,6 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +#nullable enable + +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs index 536e810c1e3a..fb62e07ab6cd 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs @@ -1,4 +1,6 @@ -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +#nullable enable + +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs index 19fce04b98fb..4e633bc2ee14 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs @@ -1,8 +1,23 @@ -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +#nullable enable + +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +/// +/// 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. +/// public interface IPolicyRequirement; +/// +/// A factory function that takes a sequence of and transforms them into a single +/// 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). +/// +/// +/// See for helpful extension methods. +/// public delegate T CreateRequirement(IEnumerable policyDetails) where T : IPolicyRequirement; From 950715ace55ebc8fdd30ca38fc6d48a3fa1bb5c7 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 11 Feb 2025 12:34:22 +1000 Subject: [PATCH 20/20] Add tests --- .../Implementations/PolicyRequirementQuery.cs | 6 +- .../Policies/PolicyRequirementQueryTests.cs | 62 +++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs index fb62e07ab6cd..20211ab29836 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs @@ -9,7 +9,7 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; public class PolicyRequirementQuery : IPolicyRequirementQuery { private readonly IPolicyRepository _policyRepository; - private readonly PolicyRequirementRegistry _policyRequirements = new(); + protected readonly PolicyRequirementRegistry PolicyRequirements = new(); public PolicyRequirementQuery(IPolicyRepository policyRepository) { @@ -19,7 +19,7 @@ public PolicyRequirementQuery(IPolicyRepository policyRepository) } public async Task GetAsync(Guid userId) where T : IPolicyRequirement - => _policyRequirements.Get()(await GetPolicyDetails(userId)); + => PolicyRequirements.Get()(await GetPolicyDetails(userId)); private Task> GetPolicyDetails(Guid userId) => _policyRepository.GetPolicyDetailsByUserId(userId); @@ -27,7 +27,7 @@ private Task> GetPolicyDetails(Guid userId) => /// /// Helper class used to register and retrieve Policy Requirement factories by type. /// - private class PolicyRequirementRegistry + protected class PolicyRequirementRegistry { private readonly Dictionary> _registry = new(); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs new file mode 100644 index 000000000000..3d009b9a6832 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs @@ -0,0 +1,62 @@ +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; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies; + +[SutProviderCustomize] +public class PolicyRequirementQueryTests +{ + /// + /// Tests that the query correctly registers, retrieves and instantiates arbitrary IPolicyRequirements + /// according to their provided CreateRequirement delegate. + /// + [Theory, BitAutoData] + public async Task GetAsync_Works(Guid userId, Guid organizationId, SutProvider sutProvider) + { + sutProvider.GetDependency().GetPolicyDetailsByUserId(userId).Returns([ + new PolicyDetails + { + OrganizationId = organizationId + } + ]); + + var requirement = await sutProvider.Sut.GetAsync(userId); + Assert.Equal(organizationId, requirement.OrganizationId); + } + + [Theory, BitAutoData] + public async Task GetAsync_ThrowsIfNoRequirementRegistered(Guid userId, SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() + => sutProvider.Sut.GetAsync(userId)); + Assert.Contains("No Policy Requirement found", exception.Message); + } + + /// + /// Test query used to register our own TestPolicyRequirement so that we're testing the query itself + /// decoupled from any real requirement that is registered from time to time. + /// + public class TestPolicyRequirementQuery : PolicyRequirementQuery + { + public TestPolicyRequirementQuery(IPolicyRepository policyRepository) : base(policyRepository) + { + PolicyRequirements.Add(TestPolicyRequirement.Create); + } + } + + /// + /// Intentionally simplified PolicyRequirement that just holds the Policy.OrganizationId for us to assert against. + /// + private class TestPolicyRequirement : IPolicyRequirement + { + public Guid OrganizationId { get; init; } + public static TestPolicyRequirement Create(IEnumerable policyDetails) + => new() { OrganizationId = policyDetails.Single().OrganizationId }; + } +}