1
0
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:
Shane Melton
2023-04-17 07:35:47 -07:00
committed by GitHub
parent 972a500745
commit f2fad5513d
13 changed files with 149 additions and 12 deletions

View File

@ -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; }
}

View 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; }
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);