Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1db8da1
[EC-1070] Add API endpoint to retrieve all policies for the current user
shane-melton Feb 10, 2023
515564d
[EC-1070] Add MasterPasswordPolicyData model
shane-melton Feb 17, 2023
5a0d36c
[EC-1070] Move PolicyResponseModel to Core project
shane-melton Feb 22, 2023
869045c
[EC-1070] Supply master password polices as a custom identity token r…
shane-melton Feb 22, 2023
aec0da7
[EC-1070] Include master password policies in 2FA token response
shane-melton Mar 1, 2023
bdfe85a
[EC-1070] Add response model to verify-password endpoint that include…
shane-melton Mar 1, 2023
97bf2db
Merge branch 'master' into EC-1070-expand-master-pass-reqs
shane-melton Mar 1, 2023
63cba51
[AC-1070] Introduce MasterPasswordPolicyResponseModel
shane-melton Mar 6, 2023
1dfa772
[AC-1070] Add policy service method to retrieve a user's master passw…
shane-melton Mar 6, 2023
af6795b
[AC-1070] User new policy service method
shane-melton Mar 6, 2023
63f4437
[AC-1070] Cleanup new policy service method
shane-melton Mar 9, 2023
b49b100
[AC-1070] Cleanup MasterPasswordPolicy models
shane-melton Mar 9, 2023
3b6566a
[AC-1070] Remove now un-used GET /policies endpoint
shane-melton Mar 9, 2023
207d0ed
[AC-1070] Update policy service method to use GetManyByUserIdAsync
shane-melton Mar 9, 2023
5054498
[AC-1070] Ensure existing value is not null before comparison
shane-melton Mar 10, 2023
9286cdf
[AC-1070] Remove redundant VerifyMasterPasswordResponse model
shane-melton Apr 5, 2023
40a4cb8
[AC-1070] Fix service typo in constructor
shane-melton Apr 5, 2023
d5d3fc6
Merge branch 'master' into EC-1070-expand-master-pass-reqs
shane-melton Apr 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Api/Auth/Controllers/EmergencyAccessController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Bit.Core.Auth.Services;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Models.Api.Response;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
Expand Down
12 changes: 9 additions & 3 deletions src/Api/Controllers/AccountsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Bit.Core.Enums;
using Bit.Core.Enums.Provider;
using Bit.Core.Exceptions;
using Bit.Core.Models.Api.Response;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
Expand Down Expand Up @@ -41,6 +42,7 @@ public class AccountsController : Controller
private readonly ISendRepository _sendRepository;
private readonly ISendService _sendService;
private readonly ICaptchaValidationService _captchaValidationService;
private readonly IPolicyService _policyService;

public AccountsController(
GlobalSettings globalSettings,
Expand All @@ -54,7 +56,8 @@ public AccountsController(
IUserService userService,
ISendRepository sendRepository,
ISendService sendService,
ICaptchaValidationService captchaValidationService)
ICaptchaValidationService captchaValidationService,
IPolicyService policyService)
{
_cipherRepository = cipherRepository;
_folderRepository = folderRepository;
Expand All @@ -68,6 +71,7 @@ public AccountsController(
_sendRepository = sendRepository;
_sendService = sendService;
_captchaValidationService = captchaValidationService;
_policyService = policyService;
}

#region DEPRECATED (Moved to Identity Service)
Expand Down Expand Up @@ -261,7 +265,7 @@ public async Task PostSetPasswordAsync([FromBody] SetPasswordRequestModel model)
}

[HttpPost("verify-password")]
public async Task PostVerifyPassword([FromBody] SecretVerificationRequestModel model)
public async Task<MasterPasswordPolicyResponseModel> PostVerifyPassword([FromBody] SecretVerificationRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
Expand All @@ -271,7 +275,9 @@ public async Task PostVerifyPassword([FromBody] SecretVerificationRequestModel m

if (await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
{
return;
var policyData = await _policyService.GetMasterPasswordPolicyForUserAsync(user);

return new MasterPasswordPolicyResponseModel(policyData);
}

ModelState.AddModelError(nameof(model.MasterPasswordHash), "Invalid password.");
Expand Down
1 change: 1 addition & 0 deletions src/Api/Controllers/PoliciesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Api.Response;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
Expand Down
1 change: 1 addition & 0 deletions src/Api/Vault/Models/Response/SyncResponseModel.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Bit.Api.Models.Response;
using Bit.Core.Entities;
using Bit.Core.Models.Api;
using Bit.Core.Models.Api.Response;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Settings;
Expand Down
36 changes: 36 additions & 0 deletions src/Core/Models/Api/Response/MasterPasswordPolicyResponseModel.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
using System.Text.Json;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Api;

namespace Bit.Api.Models.Response;
namespace Bit.Core.Models.Api.Response;

public class PolicyResponseModel : ResponseModel
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
6 changes: 6 additions & 0 deletions src/Core/Services/IPolicyService.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations.Policies;

namespace Bit.Core.Services;

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);
}
22 changes: 22 additions & 0 deletions src/Core/Services/Implementations/PolicyService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -141,6 +142,27 @@ await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
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);
Expand Down
24 changes: 22 additions & 2 deletions src/Identity/IdentityServer/BaseRequestValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Bit.Core.Enums;
using Bit.Core.Identity;
using Bit.Core.Models.Api;
using Bit.Core.Models.Api.Response;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Repositories;
using Bit.Core.Services;
Expand Down Expand Up @@ -38,6 +39,7 @@ public abstract class BaseRequestValidator<T> where T : class
private readonly GlobalSettings _globalSettings;
private readonly IPolicyRepository _policyRepository;
private readonly IUserRepository _userRepository;
private readonly IPolicyService _policyService;

public BaseRequestValidator(
UserManager<User> userManager,
Expand All @@ -54,7 +56,8 @@ public BaseRequestValidator(
ICurrentContext currentContext,
GlobalSettings globalSettings,
IPolicyRepository policyRepository,
IUserRepository userRepository)
IUserRepository userRepository,
IPolicyService policyService)
{
_userManager = userManager;
_deviceRepository = deviceRepository;
Expand All @@ -71,6 +74,7 @@ public BaseRequestValidator(
_globalSettings = globalSettings;
_policyRepository = policyRepository;
_userRepository = userRepository;
_policyService = policyService;
}

protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
Expand Down Expand Up @@ -181,6 +185,7 @@ protected async Task BuildSuccessResultAsync(User user, T context, Device device
customResponse.Add("Key", user.Key);
}

customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user));
customResponse.Add("ForcePasswordReset", user.ForcePasswordReset);
customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword));
customResponse.Add("Kdf", (byte)user.Kdf);
Expand Down Expand Up @@ -239,7 +244,8 @@ protected async Task BuildTwoFactorResultAsync(User user, Organization organizat
new Dictionary<string, object>
{
{ "TwoFactorProviders", providers.Keys },
{ "TwoFactorProviders2", providers }
{ "TwoFactorProviders2", providers },
{ "MasterPasswordPolicy", await GetMasterPasswordPolicy(user) }
});

if (enabledProviders.Count() == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email)
Expand Down Expand Up @@ -568,4 +574,18 @@ private bool ValidateFailedAuthEmailConditions(bool unknownDevice, User user)
var failedLoginCount = user?.FailedLoginCount ?? 0;
return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling;
}

private async Task<MasterPasswordPolicyResponseModel> GetMasterPasswordPolicy(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))
.ToList();

if (!orgs.Any())
{
return null;
}

return new MasterPasswordPolicyResponseModel(await _policyService.GetMasterPasswordPolicyForUserAsync(user));
}
}
5 changes: 3 additions & 2 deletions src/Identity/IdentityServer/CustomTokenRequestValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ public CustomTokenRequestValidator(
GlobalSettings globalSettings,
IPolicyRepository policyRepository,
ISsoConfigRepository ssoConfigRepository,
IUserRepository userRepository)
IUserRepository userRepository,
IPolicyService policyService)
: base(userManager, deviceRepository, deviceService, userService, eventService,
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository,
userRepository)
userRepository, policyService)
{
_userManager = userManager;
_ssoConfigRepository = ssoConfigRepository;
Expand Down
5 changes: 3 additions & 2 deletions src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ public ResourceOwnerPasswordValidator(
IPolicyRepository policyRepository,
ICaptchaValidationService captchaValidationService,
IAuthRequestRepository authRequestRepository,
IUserRepository userRepository)
IUserRepository userRepository,
IPolicyService policyService)
: base(userManager, deviceRepository, deviceService, userService, eventService,
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository,
userRepository)
userRepository, policyService)
{
_userManager = userManager;
_userService = userService;
Expand Down
5 changes: 4 additions & 1 deletion test/Api.Test/Controllers/AccountsControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public class AccountsControllerTests : IDisposable
private readonly ISendService _sendService;
private readonly IProviderUserRepository _providerUserRepository;
private readonly ICaptchaValidationService _captchaValidationService;
private readonly IPolicyService _policyService;

public AccountsControllerTests()
{
Expand All @@ -48,6 +49,7 @@ public AccountsControllerTests()
_sendRepository = Substitute.For<ISendRepository>();
_sendService = Substitute.For<ISendService>();
_captchaValidationService = Substitute.For<ICaptchaValidationService>();
_policyService = Substitute.For<IPolicyService>();
_sut = new AccountsController(
_globalSettings,
_cipherRepository,
Expand All @@ -60,7 +62,8 @@ public AccountsControllerTests()
_userService,
_sendRepository,
_sendService,
_captchaValidationService
_captchaValidationService,
_policyService
);
}

Expand Down