diff --git a/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs new file mode 100644 index 000000000..1ae93ccf5 --- /dev/null +++ b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs @@ -0,0 +1,24 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Models.Data.Organizations.OrganizationUsers; + +public class OrganizationUserPolicyDetails +{ + public Guid OrganizationUserId { get; set; } + + public Guid OrganizationId { get; set; } + + public PolicyType PolicyType { get; set; } + + public bool PolicyEnabled { get; set; } + + public string PolicyData { get; set; } + + public OrganizationUserType OrganizationUserType { get; set; } + + public OrganizationUserStatusType OrganizationUserStatus { get; set; } + + public string OrganizationUserPermissionsData { get; set; } + + public bool IsProvider { get; set; } +} diff --git a/src/Core/Repositories/IOrganizationUserRepository.cs b/src/Core/Repositories/IOrganizationUserRepository.cs index 207295b8b..16d333f9e 100644 --- a/src/Core/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/Repositories/IOrganizationUserRepository.cs @@ -39,4 +39,5 @@ public interface IOrganizationUserRepository : IRepository> GetManyByMinimumRoleAsync(Guid organizationId, OrganizationUserType minRole); Task RevokeAsync(Guid id); Task RestoreAsync(Guid id, OrganizationUserStatusType status); + Task> GetByUserIdWithPolicyDetailsAsync(Guid userId, PolicyType policyType); } diff --git a/src/Core/Repositories/IPolicyRepository.cs b/src/Core/Repositories/IPolicyRepository.cs index ce965e174..389d116c4 100644 --- a/src/Core/Repositories/IPolicyRepository.cs +++ b/src/Core/Repositories/IPolicyRepository.cs @@ -8,8 +8,4 @@ public interface IPolicyRepository : IRepository Task GetByOrganizationIdTypeAsync(Guid organizationId, PolicyType type); Task> GetManyByOrganizationIdAsync(Guid organizationId); Task> GetManyByUserIdAsync(Guid userId); - Task> GetManyByTypeApplicableToUserIdAsync(Guid userId, PolicyType policyType, - OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted); - Task GetCountByTypeApplicableToUserIdAsync(Guid userId, PolicyType policyType, - OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted); } diff --git a/src/Core/Services/IPolicyService.cs b/src/Core/Services/IPolicyService.cs index 5be4b6de0..51867ec96 100644 --- a/src/Core/Services/IPolicyService.cs +++ b/src/Core/Services/IPolicyService.cs @@ -1,4 +1,6 @@ using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.Policies; namespace Bit.Core.Services; @@ -12,4 +14,6 @@ public interface IPolicyService /// Get the combined master password policy options for the specified user. /// Task GetMasterPasswordPolicyForUserAsync(User user); + Task> GetPoliciesApplicableToUserAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted); + Task AnyPoliciesApplicableToUserAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted); } diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 428b9481c..1cdeb4268 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -42,6 +42,7 @@ public class OrganizationService : IOrganizationService private readonly IApplicationCacheService _applicationCacheService; private readonly IPaymentService _paymentService; private readonly IPolicyRepository _policyRepository; + private readonly IPolicyService _policyService; private readonly ISsoConfigRepository _ssoConfigRepository; private readonly ISsoUserRepository _ssoUserRepository; private readonly IReferenceEventService _referenceEventService; @@ -70,6 +71,7 @@ public class OrganizationService : IOrganizationService IApplicationCacheService applicationCacheService, IPaymentService paymentService, IPolicyRepository policyRepository, + IPolicyService policyService, ISsoConfigRepository ssoConfigRepository, ISsoUserRepository ssoUserRepository, IReferenceEventService referenceEventService, @@ -97,6 +99,7 @@ public class OrganizationService : IOrganizationService _applicationCacheService = applicationCacheService; _paymentService = paymentService; _policyRepository = policyRepository; + _policyService = policyService; _ssoConfigRepository = ssoConfigRepository; _ssoUserRepository = ssoUserRepository; _referenceEventService = referenceEventService; @@ -690,8 +693,8 @@ public class OrganizationService : IOrganizationService private async Task ValidateSignUpPoliciesAsync(Guid ownerId) { - var singleOrgPolicyCount = await _policyRepository.GetCountByTypeApplicableToUserIdAsync(ownerId, PolicyType.SingleOrg); - if (singleOrgPolicyCount > 0) + var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg); + if (anySingleOrgPolicies) { throw new BadRequestException("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."); @@ -1296,7 +1299,7 @@ public class OrganizationService : IOrganizationService // 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 _policyRepository.GetManyByTypeApplicableToUserIdAsync(user.Id, + var invitedSingleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg, OrganizationUserStatusType.Invited); if (hasOtherOrgs && invitedSingleOrgPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) @@ -1306,9 +1309,9 @@ public class OrganizationService : IOrganizationService } // Enforce Single Organization Policy of other organizations user is a member of - var singleOrgPolicyCount = await _policyRepository.GetCountByTypeApplicableToUserIdAsync(user.Id, + var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg); - if (singleOrgPolicyCount > 0) + if (anySingleOrgPolicies) { throw new BadRequestException("You cannot join this organization because you are a member of " + "another organization which forbids it"); @@ -1317,7 +1320,7 @@ public class OrganizationService : IOrganizationService // Enforce Two Factor Authentication Policy of organization user is trying to join if (!await userService.TwoFactorIsEnabledAsync(user)) { - var invitedTwoFactorPolicies = await _policyRepository.GetManyByTypeApplicableToUserIdAsync(user.Id, + var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) { @@ -2384,7 +2387,7 @@ public class OrganizationService : IOrganizationService // Enforce Single Organization Policy of organization user is being restored to var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(userId); var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId); - var singleOrgPoliciesApplyingToRevokedUsers = await _policyRepository.GetManyByTypeApplicableToUserIdAsync(userId, + var singleOrgPoliciesApplyingToRevokedUsers = await _policyService.GetPoliciesApplicableToUserAsync(userId, PolicyType.SingleOrg, OrganizationUserStatusType.Revoked); var singleOrgPolicyApplies = singleOrgPoliciesApplyingToRevokedUsers.Any(p => p.OrganizationId == orgUser.OrganizationId); @@ -2395,9 +2398,9 @@ public class OrganizationService : IOrganizationService } // Enforce Single Organization Policy of other organizations user is a member of - var singleOrgPolicyCount = await _policyRepository.GetCountByTypeApplicableToUserIdAsync(userId, + var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId, PolicyType.SingleOrg); - if (singleOrgPolicyCount > 0) + if (anySingleOrgPolicies) { throw new BadRequestException("You cannot restore this user because they are a member of " + "another organization which forbids it"); @@ -2407,7 +2410,7 @@ public class OrganizationService : IOrganizationService var user = await _userRepository.GetByIdAsync(userId); if (!await userService.TwoFactorIsEnabledAsync(user)) { - var invitedTwoFactorPolicies = await _policyRepository.GetManyByTypeApplicableToUserIdAsync(userId, + var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId, PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) { diff --git a/src/Core/Services/Implementations/PolicyService.cs b/src/Core/Services/Implementations/PolicyService.cs index 83020505a..ac54ff65d 100644 --- a/src/Core/Services/Implementations/PolicyService.cs +++ b/src/Core/Services/Implementations/PolicyService.cs @@ -3,8 +3,10 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.Policies; using Bit.Core.Repositories; +using Bit.Core.Settings; namespace Bit.Core.Services; @@ -16,6 +18,7 @@ public class PolicyService : IPolicyService private readonly IPolicyRepository _policyRepository; private readonly ISsoConfigRepository _ssoConfigRepository; private readonly IMailService _mailService; + private readonly GlobalSettings _globalSettings; public PolicyService( IEventService eventService, @@ -23,7 +26,8 @@ public class PolicyService : IPolicyService IOrganizationUserRepository organizationUserRepository, IPolicyRepository policyRepository, ISsoConfigRepository ssoConfigRepository, - IMailService mailService) + IMailService mailService, + GlobalSettings globalSettings) { _eventService = eventService; _organizationRepository = organizationRepository; @@ -31,6 +35,7 @@ public class PolicyService : IPolicyService _policyRepository = policyRepository; _ssoConfigRepository = ssoConfigRepository; _mailService = mailService; + _globalSettings = globalSettings; } public async Task SaveAsync(Policy policy, IUserService userService, IOrganizationService organizationService, @@ -164,6 +169,47 @@ public class PolicyService : IPolicyService return enforcedOptions; } + public async Task> GetPoliciesApplicableToUserAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted) + { + var result = await QueryOrganizationUserPolicyDetailsAsync(userId, policyType, minStatus); + return result.ToList(); + } + + public async Task AnyPoliciesApplicableToUserAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted) + { + var result = await QueryOrganizationUserPolicyDetailsAsync(userId, policyType, minStatus); + return result.Any(); + } + + private async Task> QueryOrganizationUserPolicyDetailsAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted) + { + var organizationUserPolicyDetails = await _organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(userId, policyType); + var excludedUserTypes = GetUserTypesExcludedFromPolicy(policyType); + return organizationUserPolicyDetails.Where(o => + o.PolicyEnabled && + !excludedUserTypes.Contains(o.OrganizationUserType) && + o.OrganizationUserStatus >= minStatus && + !o.IsProvider); + } + + private OrganizationUserType[] GetUserTypesExcludedFromPolicy(PolicyType policyType) + { + switch (policyType) + { + case PolicyType.MasterPassword: + return Array.Empty(); + case PolicyType.RequireSso: + // If 'EnforceSsoPolicyForAllUsers' is set to true then SSO policy applies to all user types otherwise it does not apply to Owner or Admin + if (_globalSettings.Sso.EnforceSsoPolicyForAllUsers) + { + return Array.Empty(); + } + break; + } + + return new[] { OrganizationUserType.Owner, OrganizationUserType.Admin }; + } + private async Task DependsOnSingleOrgAsync(Organization org) { var singleOrg = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.SingleOrg); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 8aa2dd85c..f1a0974a7 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -46,6 +46,7 @@ public class UserService : UserManager, IUserService, IDisposable private readonly IApplicationCacheService _applicationCacheService; private readonly IPaymentService _paymentService; private readonly IPolicyRepository _policyRepository; + private readonly IPolicyService _policyService; private readonly IDataProtector _organizationServiceDataProtector; private readonly IReferenceEventService _referenceEventService; private readonly IFido2 _fido2; @@ -77,6 +78,7 @@ public class UserService : UserManager, IUserService, IDisposable IDataProtectionProvider dataProtectionProvider, IPaymentService paymentService, IPolicyRepository policyRepository, + IPolicyService policyService, IReferenceEventService referenceEventService, IFido2 fido2, ICurrentContext currentContext, @@ -110,6 +112,7 @@ public class UserService : UserManager, IUserService, IDisposable _applicationCacheService = applicationCacheService; _paymentService = paymentService; _policyRepository = policyRepository; + _policyService = policyService; _organizationServiceDataProtector = dataProtectionProvider.CreateProtector( "OrganizationServiceDataProtector"); _referenceEventService = referenceEventService; @@ -1414,8 +1417,7 @@ public class UserService : UserManager, IUserService, IDisposable private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user, IOrganizationService organizationService) { - var twoFactorPolicies = await _policyRepository.GetManyByTypeApplicableToUserIdAsync(user.Id, - PolicyType.TwoFactorAuthentication); + var twoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication); var removeOrgUserTasks = twoFactorPolicies.Select(async p => { diff --git a/src/Core/Tools/Services/Implementations/SendService.cs b/src/Core/Tools/Services/Implementations/SendService.cs index b012d2344..5347cb010 100644 --- a/src/Core/Tools/Services/Implementations/SendService.cs +++ b/src/Core/Tools/Services/Implementations/SendService.cs @@ -24,6 +24,7 @@ public class SendService : ISendService private readonly ISendRepository _sendRepository; private readonly IUserRepository _userRepository; private readonly IPolicyRepository _policyRepository; + private readonly IPolicyService _policyService; private readonly IUserService _userService; private readonly IOrganizationRepository _organizationRepository; private readonly ISendFileStorageService _sendFileStorageService; @@ -45,12 +46,14 @@ public class SendService : ISendService IReferenceEventService referenceEventService, GlobalSettings globalSettings, IPolicyRepository policyRepository, + IPolicyService policyService, ICurrentContext currentContext) { _sendRepository = sendRepository; _userRepository = userRepository; _userService = userService; _policyRepository = policyRepository; + _policyService = policyService; _organizationRepository = organizationRepository; _sendFileStorageService = sendFileStorageService; _passwordHasher = passwordHasher; @@ -282,17 +285,17 @@ public class SendService : ISendService return; } - var disableSendPolicyCount = await _policyRepository.GetCountByTypeApplicableToUserIdAsync(userId.Value, + var anyDisableSendPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId.Value, PolicyType.DisableSend); - if (disableSendPolicyCount > 0) + if (anyDisableSendPolicies) { throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); } if (send.HideEmail.GetValueOrDefault()) { - var sendOptionsPolicies = await _policyRepository.GetManyByTypeApplicableToUserIdAsync(userId.Value, PolicyType.SendOptions); - if (sendOptionsPolicies.Any(p => p.GetDataModel()?.DisableHideEmail ?? false)) + 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."); } diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index 53254dc73..65aeb47ce 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -30,7 +30,7 @@ public class CipherService : ICipherService private readonly IAttachmentStorageService _attachmentStorageService; private readonly IEventService _eventService; private readonly IUserService _userService; - private readonly IPolicyRepository _policyRepository; + private readonly IPolicyService _policyService; private readonly GlobalSettings _globalSettings; private const long _fileSizeLeeway = 1024L * 1024L; // 1MB private readonly IReferenceEventService _referenceEventService; @@ -47,7 +47,7 @@ public class CipherService : ICipherService IAttachmentStorageService attachmentStorageService, IEventService eventService, IUserService userService, - IPolicyRepository policyRepository, + IPolicyService policyService, GlobalSettings globalSettings, IReferenceEventService referenceEventService, ICurrentContext currentContext) @@ -62,7 +62,7 @@ public class CipherService : ICipherService _attachmentStorageService = attachmentStorageService; _eventService = eventService; _userService = userService; - _policyRepository = policyRepository; + _policyService = policyService; _globalSettings = globalSettings; _referenceEventService = referenceEventService; _currentContext = currentContext; @@ -134,9 +134,8 @@ public class CipherService : ICipherService else { // Make sure the user can save new ciphers to their personal vault - var personalOwnershipPolicyCount = await _policyRepository.GetCountByTypeApplicableToUserIdAsync(savingUserId, - PolicyType.PersonalOwnership); - if (personalOwnershipPolicyCount > 0) + var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership); + if (anyPersonalOwnershipPolicies) { throw new BadRequestException("Due to an Enterprise Policy, you are restricted from saving items to your personal vault."); } @@ -632,9 +631,8 @@ public class CipherService : ICipherService var userId = folders.FirstOrDefault()?.UserId ?? ciphers.FirstOrDefault()?.UserId; // Make sure the user can save new ciphers to their personal vault - var personalOwnershipPolicyCount = await _policyRepository.GetCountByTypeApplicableToUserIdAsync(userId.Value, - PolicyType.PersonalOwnership); - if (personalOwnershipPolicyCount > 0) + var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId.Value, PolicyType.PersonalOwnership); + if (anyPersonalOwnershipPolicies) { throw new BadRequestException("You cannot import items into your personal vault because you are " + "a member of an organization which forbids it."); diff --git a/src/Identity/IdentityServer/BaseRequestValidator.cs b/src/Identity/IdentityServer/BaseRequestValidator.cs index e3bf7bbd1..3ee5c4806 100644 --- a/src/Identity/IdentityServer/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/BaseRequestValidator.cs @@ -39,9 +39,8 @@ public abstract class BaseRequestValidator where T : class private readonly ILogger _logger; private readonly ICurrentContext _currentContext; private readonly GlobalSettings _globalSettings; - private readonly IPolicyRepository _policyRepository; - private readonly IUserRepository _userRepository; private readonly IPolicyService _policyService; + private readonly IUserRepository _userRepository; private readonly IDataProtectorTokenFactory _tokenDataFactory; public BaseRequestValidator( @@ -76,7 +75,7 @@ public abstract class BaseRequestValidator where T : class _logger = logger; _currentContext = currentContext; _globalSettings = globalSettings; - _policyRepository = policyRepository; + _policyService = policyService; _userRepository = userRepository; _policyService = policyService; _tokenDataFactory = tokenDataFactory; @@ -341,33 +340,11 @@ public abstract class BaseRequestValidator where T : class return true; } - // Is user apart of any orgs? Use cache for initial checks. - var orgs = (await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)) - .ToList(); - if (orgs.Any()) + // Check if user belongs to any organization with an active SSO policy + var anySsoPoliciesApplicableToUser = await _policyService.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed); + if (anySsoPoliciesApplicableToUser) { - // Get all org abilities - var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); - // Parse all user orgs that are enabled and have the ability to use sso - var ssoOrgs = orgs.Where(o => OrgCanUseSso(orgAbilities, o.Id)); - if (ssoOrgs.Any()) - { - // Parse users orgs and determine if require sso policy is enabled - var userOrgs = await _organizationUserRepository.GetManyDetailsByUserAsync(user.Id, - OrganizationUserStatusType.Confirmed); - foreach (var userOrg in userOrgs.Where(o => o.Enabled && o.UseSso)) - { - var orgPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(userOrg.OrganizationId, - PolicyType.RequireSso); - // Owners and Admins are exempt from this policy - if (orgPolicy != null && orgPolicy.Enabled && - (_globalSettings.Sso.EnforceSsoPolicyForAllUsers || - (userOrg.Type != OrganizationUserType.Owner && userOrg.Type != OrganizationUserType.Admin))) - { - return false; - } - } - } + return false; } // Default - continue validation process @@ -380,12 +357,6 @@ public abstract class BaseRequestValidator where T : class orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa; } - private bool OrgCanUseSso(IDictionary orgAbilities, Guid orgId) - { - return orgAbilities != null && orgAbilities.ContainsKey(orgId) && - orgAbilities[orgId].Enabled && orgAbilities[orgId].UseSso; - } - private Device GetDeviceFromRequest(ValidatedRequest request) { var deviceIdentifier = request.Raw["DeviceIdentifier"]?.ToString(); diff --git a/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs index 462f1ba06..ca2469490 100644 --- a/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs @@ -491,4 +491,17 @@ public class OrganizationUserRepository : Repository, IO commandType: CommandType.StoredProcedure); } } + + public async Task> GetByUserIdWithPolicyDetailsAsync(Guid userId, PolicyType policyType) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[{Table}_ReadByUserIdWithPolicyDetails]", + new { UserId = userId, PolicyType = policyType }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } } diff --git a/src/Infrastructure.Dapper/Repositories/PolicyRepository.cs b/src/Infrastructure.Dapper/Repositories/PolicyRepository.cs index 3916a766a..8329a8a82 100644 --- a/src/Infrastructure.Dapper/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/PolicyRepository.cs @@ -56,32 +56,4 @@ public class PolicyRepository : Repository, IPolicyRepository return results.ToList(); } } - - public async Task> GetManyByTypeApplicableToUserIdAsync(Guid userId, PolicyType policyType, - OrganizationUserStatusType minStatus) - { - using (var connection = new SqlConnection(ConnectionString)) - { - var results = await connection.QueryAsync( - $"[{Schema}].[{Table}_ReadByTypeApplicableToUser]", - new { UserId = userId, PolicyType = policyType, MinimumStatus = minStatus }, - commandType: CommandType.StoredProcedure); - - return results.ToList(); - } - } - - public async Task GetCountByTypeApplicableToUserIdAsync(Guid userId, PolicyType policyType, - OrganizationUserStatusType minStatus) - { - using (var connection = new SqlConnection(ConnectionString)) - { - var result = await connection.ExecuteScalarAsync( - $"[{Schema}].[{Table}_CountByTypeApplicableToUser]", - new { UserId = userId, PolicyType = policyType, MinimumStatus = minStatus }, - commandType: CommandType.StoredProcedure); - - return result; - } - } } diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationUserRepository.cs index 8deae8e14..c9ef67121 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationUserRepository.cs @@ -587,4 +587,38 @@ public class OrganizationUserRepository : Repository> GetByUserIdWithPolicyDetailsAsync(Guid userId, PolicyType policyType) + { + 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 + let email = dbContext.Users.Find(userId).Email // Invited orgUsers do not have a UserId associated with them, so we have to match up their email + where p.Type == policyType && + (ou.UserId == userId || ou.Email == email) + select new OrganizationUserPolicyDetails + { + OrganizationUserId = ou.Id, + OrganizationId = p.OrganizationId, + PolicyType = p.Type, + PolicyEnabled = p.Enabled, + 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/src/Infrastructure.EntityFramework/Repositories/PolicyRepository.cs b/src/Infrastructure.EntityFramework/Repositories/PolicyRepository.cs index 1a02c6aa7..5dfe2b3bb 100644 --- a/src/Infrastructure.EntityFramework/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/PolicyRepository.cs @@ -48,29 +48,4 @@ public class PolicyRepository : Repository, return Mapper.Map>(results); } } - - public async Task> GetManyByTypeApplicableToUserIdAsync(Guid userId, PolicyType policyType, - OrganizationUserStatusType minStatus) - { - using (var scope = ServiceScopeFactory.CreateScope()) - { - var dbContext = GetDatabaseContext(scope); - - var query = new PolicyReadByTypeApplicableToUserQuery(userId, policyType, minStatus); - var results = await query.Run(dbContext).ToListAsync(); - return Mapper.Map>(results); - } - } - - public async Task GetCountByTypeApplicableToUserIdAsync(Guid userId, PolicyType policyType, - OrganizationUserStatusType minStatus) - { - using (var scope = ServiceScopeFactory.CreateScope()) - { - var dbContext = GetDatabaseContext(scope); - - var query = new PolicyReadByTypeApplicableToUserQuery(userId, policyType, minStatus); - return await GetCountFromQuery(query); - } - } } diff --git a/src/Infrastructure.EntityFramework/Repositories/Queries/PolicyReadByTypeApplicableToUserQuery.cs b/src/Infrastructure.EntityFramework/Repositories/Queries/PolicyReadByTypeApplicableToUserQuery.cs deleted file mode 100644 index 21e5f9a28..000000000 --- a/src/Infrastructure.EntityFramework/Repositories/Queries/PolicyReadByTypeApplicableToUserQuery.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Bit.Core.Enums; -using Bit.Infrastructure.EntityFramework.Models; - -namespace Bit.Infrastructure.EntityFramework.Repositories.Queries; - -public class PolicyReadByTypeApplicableToUserQuery : IQuery -{ - private readonly Guid _userId; - private readonly PolicyType _policyType; - private readonly OrganizationUserStatusType _minimumStatus; - - public PolicyReadByTypeApplicableToUserQuery(Guid userId, PolicyType policyType, OrganizationUserStatusType minimumStatus) - { - _userId = userId; - _policyType = policyType; - _minimumStatus = minimumStatus; - } - - public IQueryable Run(DatabaseContext dbContext) - { - var providerOrganizations = from pu in dbContext.ProviderUsers - where pu.UserId == _userId - join po in dbContext.ProviderOrganizations - on pu.ProviderId equals po.ProviderId - select po; - - string userEmail = null; - if (_minimumStatus == OrganizationUserStatusType.Invited) - { - // Invited orgUsers do not have a UserId associated with them, so we have to match up their email - userEmail = dbContext.Users.Find(_userId)?.Email; - } - - var query = from p in dbContext.Policies - join ou in dbContext.OrganizationUsers - on p.OrganizationId equals ou.OrganizationId - where - ((_minimumStatus > OrganizationUserStatusType.Invited && ou.UserId == _userId) || - (_minimumStatus == OrganizationUserStatusType.Invited && ou.Email == userEmail)) && - p.Type == _policyType && - p.Enabled && - ou.Status >= _minimumStatus && - ou.Type >= OrganizationUserType.User && - (ou.Permissions == null || - ou.Permissions.Contains($"\"managePolicies\":false")) && - !providerOrganizations.Any(po => po.OrganizationId == p.OrganizationId) - select p; - return query; - } -} diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByUserIdWithPolicyDetails.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByUserIdWithPolicyDetails.sql new file mode 100644 index 000000000..c2bc690a2 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByUserIdWithPolicyDetails.sql @@ -0,0 +1,34 @@ +CREATE PROCEDURE [dbo].[OrganizationUser_ReadByUserIdWithPolicyDetails] + @UserId UNIQUEIDENTIFIER, + @PolicyType TINYINT +AS +BEGIN + SET NOCOUNT ON +SELECT + OU.[Id] AS OrganizationUserId, + P.[OrganizationId], + P.[Type] AS PolicyType, + P.[Enabled] AS PolicyEnabled, + P.[Data] AS PolicyData, + OU.[Type] AS OrganizationUserType, + OU.[Status] AS OrganizationUserStatus, + OU.[Permissions] AS OrganizationUserPermissionsData, + CASE WHEN EXISTS ( + SELECT 1 + FROM [dbo].[ProviderUserView] PU + INNER JOIN [dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId] + WHERE PU.[UserId] = OU.[UserId] AND PO.[OrganizationId] = P.[OrganizationId] + ) THEN 1 ELSE 0 END AS IsProvider +FROM [dbo].[PolicyView] P +INNER JOIN [dbo].[OrganizationUserView] OU + ON P.[OrganizationId] = OU.[OrganizationId] +WHERE P.[Type] = @PolicyType AND + ( + (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 \ No newline at end of file diff --git a/src/Sql/dbo_future/Functions/PolicyApplicableToUser.sql b/src/Sql/dbo_future/Functions/PolicyApplicableToUser.sql new file mode 100644 index 000000000..a851a1e79 --- /dev/null +++ b/src/Sql/dbo_future/Functions/PolicyApplicableToUser.sql @@ -0,0 +1,2 @@ +-- Created 2023-03 +-- DELETE FILE \ No newline at end of file diff --git a/src/Sql/dbo_future/Stored Procedures/Policy_CountByTypeApplicableToUser.sql b/src/Sql/dbo_future/Stored Procedures/Policy_CountByTypeApplicableToUser.sql new file mode 100644 index 000000000..a851a1e79 --- /dev/null +++ b/src/Sql/dbo_future/Stored Procedures/Policy_CountByTypeApplicableToUser.sql @@ -0,0 +1,2 @@ +-- Created 2023-03 +-- DELETE FILE \ No newline at end of file diff --git a/src/Sql/dbo_future/Stored Procedures/Policy_ReadByTypeApplicableToUser.sql b/src/Sql/dbo_future/Stored Procedures/Policy_ReadByTypeApplicableToUser.sql new file mode 100644 index 000000000..a851a1e79 --- /dev/null +++ b/src/Sql/dbo_future/Stored Procedures/Policy_ReadByTypeApplicableToUser.sql @@ -0,0 +1,2 @@ +-- Created 2023-03 +-- DELETE FILE \ No newline at end of file diff --git a/test/Core.Test/Services/PolicyServiceTests.cs b/test/Core.Test/Services/PolicyServiceTests.cs index a5b2c9607..9f09e9262 100644 --- a/test/Core.Test/Services/PolicyServiceTests.cs +++ b/test/Core.Test/Services/PolicyServiceTests.cs @@ -5,12 +5,14 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; +using GlobalSettings = Bit.Core.Settings.GlobalSettings; using PolicyFixtures = Bit.Core.Test.AutoFixture.PolicyFixtures; namespace Bit.Core.Test.Services; @@ -394,10 +396,132 @@ public class PolicyServiceTests Assert.True(policy.RevisionDate - utcNow < TimeSpan.FromSeconds(1)); } + [Theory, BitAutoData] + public async Task GetPoliciesApplicableToUserAsync_WithRequireSsoTypeFilter_WithDefaultOrganizationUserStatusFilter_ReturnsNoPolicies(Guid userId, SutProvider sutProvider) + { + SetupUserPolicies(userId, sutProvider); + + var result = await sutProvider.Sut + .GetPoliciesApplicableToUserAsync(userId, PolicyType.RequireSso); + + Assert.Empty(result); + } + + [Theory, BitAutoData] + public async Task GetPoliciesApplicableToUserAsync_WithRequireSsoTypeFilter_WithDefaultOrganizationUserStatusFilter_ReturnsOnePolicy(Guid userId, SutProvider sutProvider) + { + SetupUserPolicies(userId, sutProvider); + + sutProvider.GetDependency().Sso.EnforceSsoPolicyForAllUsers.Returns(true); + + var result = await sutProvider.Sut + .GetPoliciesApplicableToUserAsync(userId, PolicyType.RequireSso); + + Assert.Single(result); + Assert.True(result.All(details => details.PolicyEnabled)); + Assert.True(result.All(details => details.PolicyType == PolicyType.RequireSso)); + Assert.True(result.All(details => details.OrganizationUserType == OrganizationUserType.Owner)); + Assert.True(result.All(details => details.OrganizationUserStatus == OrganizationUserStatusType.Confirmed)); + Assert.True(result.All(details => !details.IsProvider)); + } + + [Theory, BitAutoData] + public async Task GetPoliciesApplicableToUserAsync_WithDisableTypeFilter_WithDefaultOrganizationUserStatusFilter_ReturnsNoPolicies(Guid userId, SutProvider sutProvider) + { + SetupUserPolicies(userId, sutProvider); + + var result = await sutProvider.Sut + .GetPoliciesApplicableToUserAsync(userId, PolicyType.DisableSend); + + Assert.Empty(result); + } + + [Theory, BitAutoData] + public async Task GetPoliciesApplicableToUserAsync_WithDisableSendTypeFilter_WithInvitedUserStatusFilter_ReturnsOnePolicy(Guid userId, SutProvider sutProvider) + { + SetupUserPolicies(userId, sutProvider); + + var result = await sutProvider.Sut + .GetPoliciesApplicableToUserAsync(userId, PolicyType.DisableSend, OrganizationUserStatusType.Invited); + + Assert.Single(result); + Assert.True(result.All(details => details.PolicyEnabled)); + Assert.True(result.All(details => details.PolicyType == PolicyType.DisableSend)); + Assert.True(result.All(details => details.OrganizationUserType == OrganizationUserType.User)); + Assert.True(result.All(details => details.OrganizationUserStatus == OrganizationUserStatusType.Invited)); + Assert.True(result.All(details => !details.IsProvider)); + } + + [Theory, BitAutoData] + public async Task AnyPoliciesApplicableToUserAsync_WithRequireSsoTypeFilter_WithDefaultOrganizationUserStatusFilter_ReturnsFalse(Guid userId, SutProvider sutProvider) + { + SetupUserPolicies(userId, sutProvider); + + var result = await sutProvider.Sut + .AnyPoliciesApplicableToUserAsync(userId, PolicyType.RequireSso); + + Assert.False(result); + } + + [Theory, BitAutoData] + public async Task AnyPoliciesApplicableToUserAsync_WithRequireSsoTypeFilter_WithDefaultOrganizationUserStatusFilter_ReturnsTrue(Guid userId, SutProvider sutProvider) + { + SetupUserPolicies(userId, sutProvider); + + sutProvider.GetDependency().Sso.EnforceSsoPolicyForAllUsers.Returns(true); + + var result = await sutProvider.Sut + .AnyPoliciesApplicableToUserAsync(userId, PolicyType.RequireSso); + + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task AnyPoliciesApplicableToUserAsync_WithDisableTypeFilter_WithDefaultOrganizationUserStatusFilter_ReturnsFalse(Guid userId, SutProvider sutProvider) + { + SetupUserPolicies(userId, sutProvider); + + var result = await sutProvider.Sut + .AnyPoliciesApplicableToUserAsync(userId, PolicyType.DisableSend); + + Assert.False(result); + } + + [Theory, BitAutoData] + public async Task AnyPoliciesApplicableToUserAsync_WithDisableSendTypeFilter_WithInvitedUserStatusFilter_ReturnsTrue(Guid userId, SutProvider sutProvider) + { + SetupUserPolicies(userId, sutProvider); + + var result = await sutProvider.Sut + .AnyPoliciesApplicableToUserAsync(userId, PolicyType.DisableSend, OrganizationUserStatusType.Invited); + + Assert.True(result); + } + private static void SetupOrg(SutProvider sutProvider, Guid organizationId, Organization organization) { sutProvider.GetDependency() .GetByIdAsync(organizationId) .Returns(Task.FromResult(organization)); } + + private static void SetupUserPolicies(Guid userId, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByUserIdWithPolicyDetailsAsync(userId, PolicyType.RequireSso) + .Returns(new List + { + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.RequireSso, PolicyEnabled = false, OrganizationUserType = OrganizationUserType.Owner, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = false}, + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.RequireSso, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.Owner, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = false }, + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.RequireSso, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.Owner, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = true } + }); + + sutProvider.GetDependency() + .GetByUserIdWithPolicyDetailsAsync(userId, PolicyType.DisableSend) + .Returns(new List + { + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.DisableSend, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Invited, IsProvider = false }, + new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.DisableSend, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Invited, IsProvider = true } + }); + } } diff --git a/test/Core.Test/Tools/Services/SendServiceTests.cs b/test/Core.Test/Tools/Services/SendServiceTests.cs index 1c573e971..5ce869b82 100644 --- a/test/Core.Test/Tools/Services/SendServiceTests.cs +++ b/test/Core.Test/Tools/Services/SendServiceTests.cs @@ -3,6 +3,7 @@ using System.Text.Json; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.Policies; using Bit.Core.Repositories; using Bit.Core.Services; @@ -33,8 +34,8 @@ public class SendServiceTests send.Id = default; send.Type = sendType; - sutProvider.GetDependency().GetCountByTypeApplicableToUserIdAsync( - Arg.Any(), PolicyType.DisableSend).Returns(disableSendPolicyAppliesToUser ? 1 : 0); + sutProvider.GetDependency().AnyPoliciesApplicableToUserAsync( + Arg.Any(), PolicyType.DisableSend).Returns(disableSendPolicyAppliesToUser); } // Disable Send policy check @@ -79,10 +80,10 @@ public class SendServiceTests PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }); - sutProvider.GetDependency().GetManyByTypeApplicableToUserIdAsync( - Arg.Any(), PolicyType.SendOptions).Returns(new List + sutProvider.GetDependency().GetPoliciesApplicableToUserAsync( + Arg.Any(), PolicyType.SendOptions).Returns(new List() { - policy, + new() { PolicyType = policy.Type, PolicyData = policy.Data, OrganizationId = policy.OrganizationId, PolicyEnabled = policy.Enabled } }); } diff --git a/test/Infrastructure.EFIntegration.Test/Repositories/EqualityComparers/OrganizationUserPolicyDetailsCompare.cs b/test/Infrastructure.EFIntegration.Test/Repositories/EqualityComparers/OrganizationUserPolicyDetailsCompare.cs new file mode 100644 index 000000000..3524f893e --- /dev/null +++ b/test/Infrastructure.EFIntegration.Test/Repositories/EqualityComparers/OrganizationUserPolicyDetailsCompare.cs @@ -0,0 +1,43 @@ +using Bit.Core.Models.Data.Organizations.OrganizationUsers; + +namespace Bit.Infrastructure.EFIntegration.Test.Repositories.EqualityComparers; + +public class OrganizationUserPolicyDetailsCompare : IEqualityComparer +{ + public bool Equals(OrganizationUserPolicyDetails x, OrganizationUserPolicyDetails y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (ReferenceEquals(x, null)) + { + return false; + } + + if (ReferenceEquals(y, null)) + { + return false; + } + + if (x.GetType() != y.GetType()) + { + return false; + } + + return x.OrganizationId.Equals(y.OrganizationId) && + x.PolicyType == y.PolicyType && + x.PolicyEnabled == y.PolicyEnabled && + x.PolicyData == y.PolicyData && + x.OrganizationUserType == y.OrganizationUserType && + x.OrganizationUserStatus == y.OrganizationUserStatus && + x.OrganizationUserPermissionsData == y.OrganizationUserPermissionsData && + x.IsProvider == y.IsProvider; + } + + public int GetHashCode(OrganizationUserPolicyDetails obj) + { + return HashCode.Combine(obj.OrganizationId, (int)obj.PolicyType, obj.PolicyEnabled, obj.PolicyData, (int)obj.OrganizationUserType, (int)obj.OrganizationUserStatus, obj.OrganizationUserPermissionsData, obj.IsProvider); + } +} diff --git a/test/Infrastructure.EFIntegration.Test/Repositories/OrganizationUserRepositoryTests.cs b/test/Infrastructure.EFIntegration.Test/Repositories/OrganizationUserRepositoryTests.cs index 2becc0fc6..4f3b912b6 100644 --- a/test/Infrastructure.EFIntegration.Test/Repositories/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.EFIntegration.Test/Repositories/OrganizationUserRepositoryTests.cs @@ -1,4 +1,10 @@ -using Bit.Core.Entities; +using System.Text.Json; +using Bit.Core.Entities; +using Bit.Core.Entities.Provider; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; using Bit.Core.Test.AutoFixture.Attributes; using Bit.Infrastructure.EFIntegration.Test.AutoFixture; using Bit.Infrastructure.EFIntegration.Test.Repositories.EqualityComparers; @@ -144,4 +150,137 @@ public class OrganizationUserRepositoryTests savedSqlOrgUser = await sqlOrgUserRepo.GetByIdAsync(postSqlOrgUser.Id); Assert.True(savedSqlOrgUser == null); } + + [CiSkippedTheory] + [EfPolicyApplicableToUserInlineAutoData(OrganizationUserType.User, false, OrganizationUserStatusType.Confirmed, true, false)] // Ordinary user + [EfPolicyApplicableToUserInlineAutoData(OrganizationUserType.User, false, OrganizationUserStatusType.Invited, true, false)] // Invited user + [EfPolicyApplicableToUserInlineAutoData(OrganizationUserType.Owner, false, OrganizationUserStatusType.Confirmed, true, false)] // Owner + [EfPolicyApplicableToUserInlineAutoData(OrganizationUserType.Admin, false, OrganizationUserStatusType.Confirmed, true, false)] // Admin + [EfPolicyApplicableToUserInlineAutoData(OrganizationUserType.User, true, OrganizationUserStatusType.Confirmed, true, false)] // canManagePolicies + [EfPolicyApplicableToUserInlineAutoData(OrganizationUserType.User, false, OrganizationUserStatusType.Confirmed, true, true)] // Provider + [EfPolicyApplicableToUserInlineAutoData(OrganizationUserType.User, false, OrganizationUserStatusType.Confirmed, false, false)] // Policy disabled + [EfPolicyApplicableToUserInlineAutoData(OrganizationUserType.User, false, OrganizationUserStatusType.Confirmed, true, false)] // No policy of Type + [EfPolicyApplicableToUserInlineAutoData(OrganizationUserType.User, false, OrganizationUserStatusType.Invited, true, false)] // User not minStatus + public async void GetByUserIdWithPolicyDetailsAsync_Works_DataMatches( + // Inline data + OrganizationUserType userType, + bool canManagePolicies, + OrganizationUserStatusType orgUserStatus, + bool policyEnabled, + bool isProvider, + + // Auto data - models + Policy policy, + User user, + Organization organization, + OrganizationUser orgUser, + Provider provider, + ProviderOrganization providerOrganization, + ProviderUser providerUser, + OrganizationUserPolicyDetailsCompare equalityComparer, + + // Auto data - EF repos + List efPolicyRepository, + List efUserRepository, + List efOrganizationRepository, + List suts, + List efProviderRepository, + List efProviderOrganizationRepository, + List efProviderUserRepository, + + // Auto data - SQL repos + SqlRepo.PolicyRepository sqlPolicyRepo, + SqlRepo.UserRepository sqlUserRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo, + SqlRepo.ProviderRepository sqlProviderRepo, + SqlRepo.OrganizationUserRepository sqlOrganizationUserRepo, + SqlRepo.ProviderOrganizationRepository sqlProviderOrganizationRepo, + SqlRepo.ProviderUserRepository sqlProviderUserRepo + ) + { + // Combine EF and SQL repos into one list per type + var policyRepos = efPolicyRepository.ToList(); + policyRepos.Add(sqlPolicyRepo); + var userRepos = efUserRepository.ToList(); + userRepos.Add(sqlUserRepo); + var orgRepos = efOrganizationRepository.ToList(); + orgRepos.Add(sqlOrganizationRepo); + var orgUserRepos = suts.ToList(); + orgUserRepos.Add(sqlOrganizationUserRepo); + var providerRepos = efProviderRepository.ToList(); + providerRepos.Add(sqlProviderRepo); + var providerOrgRepos = efProviderOrganizationRepository.ToList(); + providerOrgRepos.Add(sqlProviderOrganizationRepo); + var providerUserRepos = efProviderUserRepository.ToList(); + providerUserRepos.Add(sqlProviderUserRepo); + + // Arrange data + var savedPolicyType = PolicyType.SingleOrg; + + orgUser.Type = userType; + orgUser.Status = orgUserStatus; + var permissionsData = new Permissions { ManagePolicies = canManagePolicies }; + orgUser.Permissions = JsonSerializer.Serialize(permissionsData, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }); + + policy.Enabled = policyEnabled; + policy.Type = savedPolicyType; + + var results = new List(); + + foreach (var policyRepo in policyRepos) + { + var i = policyRepos.IndexOf(policyRepo); + + // Seed database + user.CreationDate = user.RevisionDate = DateTime.Now; + var savedUser = await userRepos[i].CreateAsync(user); + var savedOrg = await orgRepos[i].CreateAsync(organization); + + // Invited orgUsers are not associated with an account yet, so they are identified by Email not UserId + if (orgUserStatus == OrganizationUserStatusType.Invited) + { + orgUser.Email = savedUser.Email; + orgUser.UserId = null; + } + else + { + orgUser.UserId = savedUser.Id; + } + + orgUser.OrganizationId = savedOrg.Id; + await orgUserRepos[i].CreateAsync(orgUser); + + if (isProvider) + { + var savedProvider = await providerRepos[i].CreateAsync(provider); + + providerOrganization.OrganizationId = savedOrg.Id; + providerOrganization.ProviderId = savedProvider.Id; + await providerOrgRepos[i].CreateAsync(providerOrganization); + + providerUser.UserId = savedUser.Id; + providerUser.ProviderId = savedProvider.Id; + await providerUserRepos[i].CreateAsync(providerUser); + } + + policy.OrganizationId = savedOrg.Id; + await policyRepo.CreateAsync(policy); + if (efPolicyRepository.Contains(policyRepo)) + { + (policyRepo as EfRepo.BaseEntityFrameworkRepository).ClearChangeTracking(); + } + + // Act + var result = await orgUserRepos[i].GetByUserIdWithPolicyDetailsAsync(savedUser.Id, policy.Type); + results.Add(result.FirstOrDefault()); + } + + // Assert + var distinctItems = results.Distinct(equalityComparer); + + Assert.Single(distinctItems); + } } diff --git a/test/Infrastructure.EFIntegration.Test/Repositories/PolicyRepositoryTests.cs b/test/Infrastructure.EFIntegration.Test/Repositories/PolicyRepositoryTests.cs index 18a2676cd..db05c52a3 100644 --- a/test/Infrastructure.EFIntegration.Test/Repositories/PolicyRepositoryTests.cs +++ b/test/Infrastructure.EFIntegration.Test/Repositories/PolicyRepositoryTests.cs @@ -1,9 +1,4 @@ -using System.Text.Json; -using Bit.Core.Entities; -using Bit.Core.Entities.Provider; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Repositories; +using Bit.Core.Entities; using Bit.Core.Test.AutoFixture.Attributes; using Bit.Infrastructure.EFIntegration.Test.AutoFixture; using Bit.Infrastructure.EFIntegration.Test.Repositories.EqualityComparers; @@ -53,143 +48,4 @@ public class PolicyRepositoryTests var distinctItems = savedPolicys.Distinct(equalityComparer); Assert.True(!distinctItems.Skip(1).Any()); } - - [CiSkippedTheory] - [EfPolicyApplicableToUserInlineAutoData(OrganizationUserType.User, false, OrganizationUserStatusType.Confirmed, false, true, true, false)] // Ordinary user - [EfPolicyApplicableToUserInlineAutoData(OrganizationUserType.User, false, OrganizationUserStatusType.Invited, true, true, true, false)] // Invited user - [EfPolicyApplicableToUserInlineAutoData(OrganizationUserType.Owner, false, OrganizationUserStatusType.Confirmed, false, true, true, false)] // Owner - [EfPolicyApplicableToUserInlineAutoData(OrganizationUserType.Admin, false, OrganizationUserStatusType.Confirmed, false, true, true, false)] // Admin - [EfPolicyApplicableToUserInlineAutoData(OrganizationUserType.User, true, OrganizationUserStatusType.Confirmed, false, true, true, false)] // canManagePolicies - [EfPolicyApplicableToUserInlineAutoData(OrganizationUserType.User, false, OrganizationUserStatusType.Confirmed, false, true, true, true)] // Provider - [EfPolicyApplicableToUserInlineAutoData(OrganizationUserType.User, false, OrganizationUserStatusType.Confirmed, false, false, true, false)] // Policy disabled - [EfPolicyApplicableToUserInlineAutoData(OrganizationUserType.User, false, OrganizationUserStatusType.Confirmed, false, true, false, false)] // No policy of Type - [EfPolicyApplicableToUserInlineAutoData(OrganizationUserType.User, false, OrganizationUserStatusType.Invited, false, true, true, false)] // User not minStatus - - public async void GetManyByTypeApplicableToUser_Works_DataMatches( - // Inline data - OrganizationUserType userType, - bool canManagePolicies, - OrganizationUserStatusType orgUserStatus, - bool includeInvited, - bool policyEnabled, - bool policySameType, - bool isProvider, - - // Auto data - models - Policy policy, - User user, - Organization organization, - OrganizationUser orgUser, - Provider provider, - ProviderOrganization providerOrganization, - ProviderUser providerUser, - PolicyCompareIncludingOrganization equalityComparer, - - // Auto data - EF repos - List suts, - List efUserRepository, - List efOrganizationRepository, - List efOrganizationUserRepository, - List efProviderRepository, - List efProviderOrganizationRepository, - List efProviderUserRepository, - - // Auto data - SQL repos - SqlRepo.PolicyRepository sqlPolicyRepo, - SqlRepo.UserRepository sqlUserRepo, - SqlRepo.OrganizationRepository sqlOrganizationRepo, - SqlRepo.ProviderRepository sqlProviderRepo, - SqlRepo.OrganizationUserRepository sqlOrganizationUserRepo, - SqlRepo.ProviderOrganizationRepository sqlProviderOrganizationRepo, - SqlRepo.ProviderUserRepository sqlProviderUserRepo - ) - { - // Combine EF and SQL repos into one list per type - var policyRepos = suts.ToList(); - policyRepos.Add(sqlPolicyRepo); - var userRepos = efUserRepository.ToList(); - userRepos.Add(sqlUserRepo); - var orgRepos = efOrganizationRepository.ToList(); - orgRepos.Add(sqlOrganizationRepo); - var orgUserRepos = efOrganizationUserRepository.ToList(); - orgUserRepos.Add(sqlOrganizationUserRepo); - var providerRepos = efProviderRepository.ToList(); - providerRepos.Add(sqlProviderRepo); - var providerOrgRepos = efProviderOrganizationRepository.ToList(); - providerOrgRepos.Add(sqlProviderOrganizationRepo); - var providerUserRepos = efProviderUserRepository.ToList(); - providerUserRepos.Add(sqlProviderUserRepo); - - // Arrange data - var savedPolicyType = PolicyType.SingleOrg; - var queriedPolicyType = policySameType ? savedPolicyType : PolicyType.DisableSend; - - orgUser.Type = userType; - orgUser.Status = orgUserStatus; - var permissionsData = new Permissions { ManagePolicies = canManagePolicies }; - orgUser.Permissions = JsonSerializer.Serialize(permissionsData, new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }); - - policy.Enabled = policyEnabled; - policy.Type = savedPolicyType; - - var results = new List(); - - foreach (var policyRepo in policyRepos) - { - var i = policyRepos.IndexOf(policyRepo); - - // Seed database - var savedUser = await userRepos[i].CreateAsync(user); - var savedOrg = await orgRepos[i].CreateAsync(organization); - - // Invited orgUsers are not associated with an account yet, so they are identified by Email not UserId - if (orgUserStatus == OrganizationUserStatusType.Invited) - { - orgUser.Email = savedUser.Email; - orgUser.UserId = null; - } - else - { - orgUser.UserId = savedUser.Id; - } - - orgUser.OrganizationId = savedOrg.Id; - await orgUserRepos[i].CreateAsync(orgUser); - - if (isProvider) - { - var savedProvider = await providerRepos[i].CreateAsync(provider); - - providerOrganization.OrganizationId = savedOrg.Id; - providerOrganization.ProviderId = savedProvider.Id; - await providerOrgRepos[i].CreateAsync(providerOrganization); - - providerUser.UserId = savedUser.Id; - providerUser.ProviderId = savedProvider.Id; - await providerUserRepos[i].CreateAsync(providerUser); - } - - policy.OrganizationId = savedOrg.Id; - await policyRepo.CreateAsync(policy); - if (suts.Contains(policyRepo)) - { - (policyRepo as EfRepo.BaseEntityFrameworkRepository).ClearChangeTracking(); - } - - var minStatus = includeInvited ? OrganizationUserStatusType.Invited : OrganizationUserStatusType.Accepted; - - // Act - var result = await policyRepo.GetManyByTypeApplicableToUserIdAsync(savedUser.Id, queriedPolicyType, minStatus); - results.Add(result.FirstOrDefault()); - } - - // Assert - var distinctItems = results.Distinct(equalityComparer); - - Assert.True(results.All(r => r == null) || - !distinctItems.Skip(1).Any()); - } } diff --git a/util/Migrator/DbScripts/2023-03-10_00_OrganizationUserReadByUserIdWithPolicyDetails.sql b/util/Migrator/DbScripts/2023-03-10_00_OrganizationUserReadByUserIdWithPolicyDetails.sql new file mode 100644 index 000000000..7da87f4ae --- /dev/null +++ b/util/Migrator/DbScripts/2023-03-10_00_OrganizationUserReadByUserIdWithPolicyDetails.sql @@ -0,0 +1,35 @@ +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_ReadByUserIdWithPolicyDetails] + @UserId UNIQUEIDENTIFIER, + @PolicyType TINYINT +AS +BEGIN + SET NOCOUNT ON +SELECT + OU.[Id] AS OrganizationUserId, + P.[OrganizationId], + P.[Type] AS PolicyType, + P.[Enabled] AS PolicyEnabled, + P.[Data] AS PolicyData, + OU.[Type] AS OrganizationUserType, + OU.[Status] AS OrganizationUserStatus, + OU.[Permissions] AS OrganizationUserPermissionsData, + CASE WHEN EXISTS ( + SELECT 1 + FROM [dbo].[ProviderUserView] PU + INNER JOIN [dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId] + WHERE PU.[UserId] = OU.[UserId] AND PO.[OrganizationId] = P.[OrganizationId] + ) THEN 1 ELSE 0 END AS IsProvider +FROM [dbo].[PolicyView] P +INNER JOIN [dbo].[OrganizationUserView] OU + ON P.[OrganizationId] = OU.[OrganizationId] +WHERE P.[Type] = @PolicyType AND + ( + (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 +GO \ No newline at end of file diff --git a/util/Migrator/DbScripts_future/2023-03-FutureMigration.sql b/util/Migrator/DbScripts_future/2023-03-FutureMigration.sql new file mode 100644 index 000000000..f3d2a5e9d --- /dev/null +++ b/util/Migrator/DbScripts_future/2023-03-FutureMigration.sql @@ -0,0 +1,11 @@ +-- Stored Procedure: Policy_CountByTypeApplicableToUser +DROP PROCEDURE [dbo].[Policy_CountByTypeApplicableToUser]; +GO + +-- Stored Procedure: Policy_ReadByTypeApplicableToUser +DROP PROCEDURE [dbo].[Policy_ReadByTypeApplicableToUser]; +GO + +-- Function: PolicyApplicableToUser +DROP FUNCTION [dbo].[PolicyApplicableToUser]; +GO \ No newline at end of file