mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -05:00
[AC-1070] Enforce master password policy on login (#2714)
* [EC-1070] Add API endpoint to retrieve all policies for the current user The additional API endpoint is required to avoid forcing a full sync call before every login for master password policy enforcement on login. * [EC-1070] Add MasterPasswordPolicyData model * [EC-1070] Move PolicyResponseModel to Core project The response model is used by both the Identity and Api projects. * [EC-1070] Supply master password polices as a custom identity token response * [EC-1070] Include master password policies in 2FA token response * [EC-1070] Add response model to verify-password endpoint that includes master password policies * [AC-1070] Introduce MasterPasswordPolicyResponseModel * [AC-1070] Add policy service method to retrieve a user's master password policy * [AC-1070] User new policy service method - Update BaseRequestValidator - Update AccountsController for /verify-password endpoint - Update VerifyMasterPasswordResponseModel to accept MasterPasswordPolicyData * [AC-1070] Cleanup new policy service method - Use User object instead of Guid - Remove TODO message - Use `PolicyRepository.GetManyByTypeApplicableToUserIdAsync` instead of filtering locally * [AC-1070] Cleanup MasterPasswordPolicy models - Remove default values from both models - Add missing `RequireLower` - Fix mismatched properties in `CombineWith` method - Make properties nullable in response model * [AC-1070] Remove now un-used GET /policies endpoint * [AC-1070] Update policy service method to use GetManyByUserIdAsync * [AC-1070] Ensure existing value is not null before comparison * [AC-1070] Remove redundant VerifyMasterPasswordResponse model * [AC-1070] Fix service typo in constructor
This commit is contained in:
@ -0,0 +1,36 @@
|
||||
using Bit.Core.Models.Data.Organizations.Policies;
|
||||
|
||||
namespace Bit.Core.Models.Api.Response;
|
||||
|
||||
public class MasterPasswordPolicyResponseModel : ResponseModel
|
||||
{
|
||||
public MasterPasswordPolicyResponseModel(MasterPasswordPolicyData data) : base("masterPasswordPolicy")
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MinComplexity = data.MinComplexity;
|
||||
MinLength = data.MinLength;
|
||||
RequireLower = data.RequireLower;
|
||||
RequireUpper = data.RequireUpper;
|
||||
RequireNumbers = data.RequireNumbers;
|
||||
RequireSpecial = data.RequireSpecial;
|
||||
EnforceOnLogin = data.EnforceOnLogin;
|
||||
}
|
||||
|
||||
public int? MinComplexity { get; set; }
|
||||
|
||||
public int? MinLength { get; set; }
|
||||
|
||||
public bool? RequireLower { get; set; }
|
||||
|
||||
public bool? RequireUpper { get; set; }
|
||||
|
||||
public bool? RequireNumbers { get; set; }
|
||||
|
||||
public bool? RequireSpecial { get; set; }
|
||||
|
||||
public bool? EnforceOnLogin { get; set; }
|
||||
}
|
32
src/Core/Models/Api/Response/PolicyResponseModel.cs
Normal file
32
src/Core/Models/Api/Response/PolicyResponseModel.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Api.Response;
|
||||
|
||||
public class PolicyResponseModel : ResponseModel
|
||||
{
|
||||
public PolicyResponseModel(Policy policy, string obj = "policy")
|
||||
: base(obj)
|
||||
{
|
||||
if (policy == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(policy));
|
||||
}
|
||||
|
||||
Id = policy.Id.ToString();
|
||||
OrganizationId = policy.OrganizationId.ToString();
|
||||
Type = policy.Type;
|
||||
Enabled = policy.Enabled;
|
||||
if (!string.IsNullOrWhiteSpace(policy.Data))
|
||||
{
|
||||
Data = JsonSerializer.Deserialize<Dictionary<string, object>>(policy.Data);
|
||||
}
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
public string OrganizationId { get; set; }
|
||||
public PolicyType Type { get; set; }
|
||||
public Dictionary<string, object> Data { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
namespace Bit.Core.Models.Data.Organizations.Policies;
|
||||
|
||||
public class MasterPasswordPolicyData : IPolicyDataModel
|
||||
{
|
||||
public int? MinComplexity { get; set; }
|
||||
public int? MinLength { get; set; }
|
||||
public bool? RequireLower { get; set; }
|
||||
public bool? RequireUpper { get; set; }
|
||||
public bool? RequireNumbers { get; set; }
|
||||
public bool? RequireSpecial { get; set; }
|
||||
public bool? EnforceOnLogin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Combine the other policy data with this instance, taking the most secure options
|
||||
/// </summary>
|
||||
/// <param name="other">The other policy instance to combine with this</param>
|
||||
public void CombineWith(MasterPasswordPolicyData other)
|
||||
{
|
||||
if (other == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (other.MinComplexity.HasValue && (!MinComplexity.HasValue || other.MinComplexity > MinComplexity))
|
||||
{
|
||||
MinComplexity = other.MinComplexity;
|
||||
}
|
||||
|
||||
if (other.MinLength.HasValue && (!MinLength.HasValue || other.MinLength > MinLength))
|
||||
{
|
||||
MinLength = other.MinLength;
|
||||
}
|
||||
|
||||
RequireLower = (other.RequireLower ?? false) || (RequireLower ?? false);
|
||||
RequireUpper = (other.RequireUpper ?? false) || (RequireUpper ?? false);
|
||||
RequireNumbers = (other.RequireNumbers ?? false) || (RequireNumbers ?? false);
|
||||
RequireSpecial = (other.RequireSpecial ?? false) || (RequireSpecial ?? false);
|
||||
EnforceOnLogin = (other.EnforceOnLogin ?? false) || (EnforceOnLogin ?? false);
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data.Organizations.Policies;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
@ -6,4 +7,9 @@ public interface IPolicyService
|
||||
{
|
||||
Task SaveAsync(Policy policy, IUserService userService, IOrganizationService organizationService,
|
||||
Guid? savingUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Get the combined master password policy options for the specified user.
|
||||
/// </summary>
|
||||
Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
@ -141,6 +142,27 @@ public class PolicyService : IPolicyService
|
||||
await _eventService.LogPolicyEventAsync(policy, Enums.EventType.Policy_Updated);
|
||||
}
|
||||
|
||||
public async Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user)
|
||||
{
|
||||
var policies = (await _policyRepository.GetManyByUserIdAsync(user.Id))
|
||||
.Where(p => p.Type == PolicyType.MasterPassword && p.Enabled)
|
||||
.ToList();
|
||||
|
||||
if (!policies.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var enforcedOptions = new MasterPasswordPolicyData();
|
||||
|
||||
foreach (var policy in policies)
|
||||
{
|
||||
enforcedOptions.CombineWith(policy.GetDataModel<MasterPasswordPolicyData>());
|
||||
}
|
||||
|
||||
return enforcedOptions;
|
||||
}
|
||||
|
||||
private async Task DependsOnSingleOrgAsync(Organization org)
|
||||
{
|
||||
var singleOrg = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.SingleOrg);
|
||||
|
Reference in New Issue
Block a user