From 7ed190006b7de42e091275f9a36b5ef95e98fc3e Mon Sep 17 00:00:00 2001 From: Patrick Pimentel Date: Thu, 5 Jun 2025 20:39:14 -0400 Subject: [PATCH] feat(change-password-component): Change Password Update [18720] - Now sending back accepted mp policies on base validator --- src/Api/Vault/Controllers/SyncController.cs | 9 ++-- .../Repositories/IPolicyRepository.cs | 1 + .../AdminConsole/Services/IPolicyService.cs | 2 +- .../Services/Implementations/PolicyService.cs | 8 +++- src/Core/Context/CurrentContext.cs | 17 +++++++ src/Core/Context/ICurrentContext.cs | 3 ++ .../RequestValidators/BaseRequestValidator.cs | 8 ++-- .../Repositories/PolicyRepository.cs | 13 +++++ .../Repositories/PolicyRepository.cs | 48 ++++++++++--------- ...icyReadAcceptedOrConfirmedByUserIdQuery.cs | 31 ++++++++++++ .../Policy_ReadAcceptOrConfirmedByUserId.sql | 18 +++++++ ...icyReadReadAcceptedOrConfirmedByUserId.sql | 18 +++++++ 12 files changed, 144 insertions(+), 32 deletions(-) create mode 100644 src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/PolicyReadAcceptedOrConfirmedByUserIdQuery.cs create mode 100644 src/Sql/dbo/Stored Procedures/Policy_ReadAcceptOrConfirmedByUserId.sql create mode 100644 util/Migrator/DbScripts/2025-06-04_00_PolicyReadReadAcceptedOrConfirmedByUserId.sql diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index 568c05d651..e18247fc88 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -81,12 +81,15 @@ public class SyncController : Controller throw new BadRequestException("User not found."); } - var organizationUserDetails = await _organizationUserRepository.GetManyDetailsByUserAsync(user.Id, + var organizationUserDetails = await _organizationUserRepository.GetManyDetailsByUserAsync( + user.Id, OrganizationUserStatusType.Confirmed); - var providerUserDetails = await _providerUserRepository.GetManyDetailsByUserAsync(user.Id, + var providerUserDetails = await _providerUserRepository.GetManyDetailsByUserAsync( + user.Id, ProviderUserStatusType.Confirmed); var providerUserOrganizationDetails = - await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id, + await _providerUserRepository.GetManyOrganizationDetailsByUserAsync( + user.Id, ProviderUserStatusType.Confirmed); var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled); diff --git a/src/Core/AdminConsole/Repositories/IPolicyRepository.cs b/src/Core/AdminConsole/Repositories/IPolicyRepository.cs index 4c0c03536d..527f4c8177 100644 --- a/src/Core/AdminConsole/Repositories/IPolicyRepository.cs +++ b/src/Core/AdminConsole/Repositories/IPolicyRepository.cs @@ -20,6 +20,7 @@ public interface IPolicyRepository : IRepository Task GetByOrganizationIdTypeAsync(Guid organizationId, PolicyType type); Task> GetManyByOrganizationIdAsync(Guid organizationId); Task> GetManyByUserIdAsync(Guid userId); + Task> GetManyAcceptedOrConfirmedByUserIdAsync(Guid userId); /// /// Gets all PolicyDetails for a user for all policy types. /// diff --git a/src/Core/AdminConsole/Services/IPolicyService.cs b/src/Core/AdminConsole/Services/IPolicyService.cs index 4f9a25f904..d2674d6abd 100644 --- a/src/Core/AdminConsole/Services/IPolicyService.cs +++ b/src/Core/AdminConsole/Services/IPolicyService.cs @@ -11,7 +11,7 @@ public interface IPolicyService /// /// Get the combined master password policy options for the specified user. /// - Task GetMasterPasswordPolicyForUserAsync(User user); + Task GetMasterPasswordPolicyForUserAsync(User user, bool getConfirmedOrAccepted = false); 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/AdminConsole/Services/Implementations/PolicyService.cs b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs index d424bd8fff..676293d669 100644 --- a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs +++ b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs @@ -29,9 +29,13 @@ public class PolicyService : IPolicyService _globalSettings = globalSettings; } - public async Task GetMasterPasswordPolicyForUserAsync(User user) + public async Task GetMasterPasswordPolicyForUserAsync(User user, bool getConfirmedOrAccepted = false) { - var policies = (await _policyRepository.GetManyByUserIdAsync(user.Id)) + var policies = getConfirmedOrAccepted ? + (await _policyRepository.GetManyAcceptedOrConfirmedByUserIdAsync(user.Id)) + .Where(p => p.Type == PolicyType.MasterPassword && p.Enabled) + .ToList() + : (await _policyRepository.GetManyByUserIdAsync(user.Id)) .Where(p => p.Type == PolicyType.MasterPassword && p.Enabled) .ToList(); diff --git a/src/Core/Context/CurrentContext.cs b/src/Core/Context/CurrentContext.cs index 68d4606907..44ea57c297 100644 --- a/src/Core/Context/CurrentContext.cs +++ b/src/Core/Context/CurrentContext.cs @@ -32,6 +32,7 @@ public class CurrentContext : ICurrentContext public virtual string IpAddress { get; set; } public virtual string CountryName { get; set; } public virtual List Organizations { get; set; } + public virtual List OrganizationsConfirmedOrAccepted { get; set; } public virtual List Providers { get; set; } public virtual Guid? InstallationId { get; set; } public virtual Guid? OrganizationId { get; set; } @@ -481,6 +482,22 @@ public class CurrentContext : ICurrentContext return Organizations; } + public async Task> OrganizationAcceptedOrConfirmedAsync( + IOrganizationUserRepository organizationUserRepository, Guid userId) + { + if (OrganizationsConfirmedOrAccepted == null) + { + // If we haven't had our user id set, take the one passed in since we are about to get information + // for them anyways. + UserId ??= userId; + + var userOrgs = await organizationUserRepository.GetManyDetailsByUserAsync(userId); + OrganizationsConfirmedOrAccepted = userOrgs.Where(ou => ou.Status == OrganizationUserStatusType.Confirmed || ou.Status == OrganizationUserStatusType.Accepted) + .Select(ou => new CurrentContextOrganization(ou)).ToList(); + } + return OrganizationsConfirmedOrAccepted; + } + public async Task> ProviderMembershipAsync( IProviderUserRepository providerUserRepository, Guid userId) { diff --git a/src/Core/Context/ICurrentContext.cs b/src/Core/Context/ICurrentContext.cs index 42843ce6d7..c597632509 100644 --- a/src/Core/Context/ICurrentContext.cs +++ b/src/Core/Context/ICurrentContext.cs @@ -70,6 +70,9 @@ public interface ICurrentContext Task> OrganizationMembershipAsync( IOrganizationUserRepository organizationUserRepository, Guid userId); + Task> OrganizationAcceptedOrConfirmedAsync( + IOrganizationUserRepository organizationUserRepository, Guid userId); + Task> ProviderMembershipAsync( IProviderUserRepository providerUserRepository, Guid userId); diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index dd4592aa0d..2d94703873 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -361,7 +361,7 @@ public abstract class BaseRequestValidator where T : class private async Task GetMasterPasswordPolicyAsync(User user) { // Check current context/cache to see if user is in any organizations, avoids extra DB call if not - var orgs = (await CurrentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)) + var orgs = (await CurrentContext.OrganizationAcceptedOrConfirmedAsync(_organizationUserRepository, user.Id)) .ToList(); if (orgs.Count == 0) @@ -369,7 +369,7 @@ public abstract class BaseRequestValidator where T : class return null; } - return new MasterPasswordPolicyResponseModel(await PolicyService.GetMasterPasswordPolicyForUserAsync(user)); + return new MasterPasswordPolicyResponseModel(await PolicyService.GetMasterPasswordPolicyForUserAsync(user, true)); } /// @@ -401,8 +401,8 @@ public abstract class BaseRequestValidator where T : class /// /// Builds the custom response that will be sent to the client upon successful authentication, which /// includes the information needed for the client to initialize the user's account in state. - /// - /// The authenticated user. + /// + /// The authenticated user. /// The current request context. /// The device used for authentication. /// Whether to send a 2FA remember token. diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs index 071ff3153a..1b89860e0a 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs @@ -61,6 +61,19 @@ public class PolicyRepository : Repository, IPolicyRepository } } + public async Task> GetManyAcceptedOrConfirmedByUserIdAsync(Guid userId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[{Table}_ReadAcceptedOrConfirmedByUserId]", + new { UserId = userId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + public async Task> GetPolicyDetailsByUserId(Guid userId) { using (var connection = new SqlConnection(ConnectionString)) diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs index 0564681341..7869051f6a 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs @@ -20,37 +20,41 @@ public class PolicyRepository : Repository GetByOrganizationIdTypeAsync(Guid organizationId, PolicyType type) { - using (var scope = ServiceScopeFactory.CreateScope()) - { - var dbContext = GetDatabaseContext(scope); - var results = await dbContext.Policies - .FirstOrDefaultAsync(p => p.OrganizationId == organizationId && p.Type == type); - return Mapper.Map(results); - } + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var results = await dbContext.Policies + .FirstOrDefaultAsync(p => p.OrganizationId == organizationId && p.Type == type); + return Mapper.Map(results); } public async Task> GetManyByOrganizationIdAsync(Guid organizationId) { - using (var scope = ServiceScopeFactory.CreateScope()) - { - var dbContext = GetDatabaseContext(scope); - var results = await dbContext.Policies - .Where(p => p.OrganizationId == organizationId) - .ToListAsync(); - return Mapper.Map>(results); - } + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var results = await dbContext.Policies + .Where(p => p.OrganizationId == organizationId) + .ToListAsync(); + return Mapper.Map>(results); } public async Task> GetManyByUserIdAsync(Guid userId) { - using (var scope = ServiceScopeFactory.CreateScope()) - { - var dbContext = GetDatabaseContext(scope); + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); - var query = new PolicyReadByUserIdQuery(userId); - var results = await query.Run(dbContext).ToListAsync(); - return Mapper.Map>(results); - } + var query = new PolicyReadByUserIdQuery(userId); + var results = await query.Run(dbContext).ToListAsync(); + return Mapper.Map>(results); + } + + public async Task> GetManyAcceptedOrConfirmedByUserIdAsync(Guid userId) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + var query = new PolicyReadAcceptedOrConfirmedByUserIdQuery(userId); + var results = await query.Run(dbContext).ToListAsync(); + return Mapper.Map>(results); } public async Task> GetPolicyDetailsByUserId(Guid userId) diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/PolicyReadAcceptedOrConfirmedByUserIdQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/PolicyReadAcceptedOrConfirmedByUserIdQuery.cs new file mode 100644 index 0000000000..45b88b91f5 --- /dev/null +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/PolicyReadAcceptedOrConfirmedByUserIdQuery.cs @@ -0,0 +1,31 @@ +using Bit.Core.Enums; +using Bit.Infrastructure.EntityFramework.AdminConsole.Models; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories.Queries; + +namespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries; + +public class PolicyReadAcceptedOrConfirmedByUserIdQuery : IQuery +{ + private readonly Guid _userId; + + public PolicyReadAcceptedOrConfirmedByUserIdQuery(Guid userId) + { + _userId = userId; + } + + public IQueryable Run(DatabaseContext dbContext) + { + var query = from p in dbContext.Policies + join ou in dbContext.OrganizationUsers + on p.OrganizationId equals ou.OrganizationId + join o in dbContext.Organizations + on ou.OrganizationId equals o.Id + where ou.UserId == _userId && + (ou.Status == OrganizationUserStatusType.Confirmed + || ou.Status == OrganizationUserStatusType.Accepted) + select p; + + return query; + } +} diff --git a/src/Sql/dbo/Stored Procedures/Policy_ReadAcceptOrConfirmedByUserId.sql b/src/Sql/dbo/Stored Procedures/Policy_ReadAcceptOrConfirmedByUserId.sql new file mode 100644 index 0000000000..c86d3739aa --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Policy_ReadAcceptOrConfirmedByUserId.sql @@ -0,0 +1,18 @@ +CREATE PROCEDURE [dbo].[Policy_ReadAcceptedOrConfirmedByUserId] + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + P.* + FROM + [dbo].[PolicyView] P + INNER JOIN + [dbo].[OrganizationUser] OU ON P.[OrganizationId] = OU.[OrganizationId] + INNER JOIN + [dbo].[Organization] O ON OU.[OrganizationId] = O.[Id] + WHERE + OU.[UserId] = @UserId + AND (OU.[Status] = 1 OR OU.[Status] = 2) -- 1 = Accepted, 2 = Confirmed +END diff --git a/util/Migrator/DbScripts/2025-06-04_00_PolicyReadReadAcceptedOrConfirmedByUserId.sql b/util/Migrator/DbScripts/2025-06-04_00_PolicyReadReadAcceptedOrConfirmedByUserId.sql new file mode 100644 index 0000000000..9f5a189c4b --- /dev/null +++ b/util/Migrator/DbScripts/2025-06-04_00_PolicyReadReadAcceptedOrConfirmedByUserId.sql @@ -0,0 +1,18 @@ +CREATE OR ALTER PROCEDURE [dbo].[Policy_ReadAcceptedOrConfirmedByUserId] + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + P.* + FROM + [dbo].[PolicyView] P + INNER JOIN + [dbo].[OrganizationUser] OU ON P.[OrganizationId] = OU.[OrganizationId] + INNER JOIN + [dbo].[Organization] O ON OU.[OrganizationId] = O.[Id] + WHERE + OU.[UserId] = @UserId + AND (OU.[Status] = 1 OR OU.[Status] = 2) -- 1 = Accepted, 2 = Confirmed +END