diff --git a/src/Core/AdminConsole/Models/Data/Organizations/Policies/MasterPasswordPolicyData.cs b/src/Core/AdminConsole/Models/Data/Organizations/Policies/MasterPasswordPolicyData.cs
index b66244ba5ff2..228d7a26f13d 100644
--- a/src/Core/AdminConsole/Models/Data/Organizations/Policies/MasterPasswordPolicyData.cs
+++ b/src/Core/AdminConsole/Models/Data/Organizations/Policies/MasterPasswordPolicyData.cs
@@ -1,11 +1,21 @@
-using System.Text.Json.Serialization;
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
public class MasterPasswordPolicyData : IPolicyDataModel
{
+ ///
+ /// Minimum password complexity score (0-4). Null indicates no complexity requirement.
+ ///
[JsonPropertyName("minComplexity")]
+ [Range(0, 4)]
public int? MinComplexity { get; set; }
+
+ ///
+ /// Minimum password length (12-128). Null indicates no minimum length requirement.
+ ///
[JsonPropertyName("minLength")]
+ [Range(12, 128)]
public int? MinLength { get; set; }
[JsonPropertyName("requireLower")]
public bool? RequireLower { get; set; }
diff --git a/src/Core/AdminConsole/Utilities/PolicyDataValidator.cs b/src/Core/AdminConsole/Utilities/PolicyDataValidator.cs
index 84e63f2a2049..d533ca88cf8b 100644
--- a/src/Core/AdminConsole/Utilities/PolicyDataValidator.cs
+++ b/src/Core/AdminConsole/Utilities/PolicyDataValidator.cs
@@ -1,4 +1,5 @@
-using System.Text.Json;
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
@@ -30,7 +31,8 @@ public static class PolicyDataValidator
switch (policyType)
{
case PolicyType.MasterPassword:
- CoreHelpers.LoadClassFromJsonData(json);
+ var masterPasswordData = CoreHelpers.LoadClassFromJsonData(json);
+ ValidateModel(masterPasswordData, policyType);
break;
case PolicyType.SendOptions:
CoreHelpers.LoadClassFromJsonData(json);
@@ -44,11 +46,24 @@ public static class PolicyDataValidator
}
catch (JsonException ex)
{
- var fieldInfo = !string.IsNullOrEmpty(ex.Path) ? $": field '{ex.Path}' has invalid type" : "";
+ var fieldName = !string.IsNullOrEmpty(ex.Path) ? ex.Path.TrimStart('$', '.') : null;
+ var fieldInfo = !string.IsNullOrEmpty(fieldName) ? $": {fieldName} has an invalid value" : "";
throw new BadRequestException($"Invalid data for {policyType} policy{fieldInfo}.");
}
}
+ private static void ValidateModel(object model, PolicyType policyType)
+ {
+ var validationContext = new ValidationContext(model);
+ var validationResults = new List();
+
+ if (!Validator.TryValidateObject(model, validationContext, validationResults, true))
+ {
+ var errors = string.Join(", ", validationResults.Select(r => r.ErrorMessage));
+ throw new BadRequestException($"Invalid data for {policyType} policy: {errors}");
+ }
+ }
+
///
/// Validates and deserializes policy metadata based on the policy type.
///
diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/PoliciesControllerTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/PoliciesControllerTests.cs
index e4098ce9a9de..d58538ae1cb2 100644
--- a/test/Api.IntegrationTest/AdminConsole/Controllers/PoliciesControllerTests.cs
+++ b/test/Api.IntegrationTest/AdminConsole/Controllers/PoliciesControllerTests.cs
@@ -150,8 +150,8 @@ public async Task PutVNext_MasterPasswordPolicy_Success()
Enabled = true,
Data = new Dictionary
{
- { "minComplexity", 10 },
- { "minLength", 12 },
+ { "minComplexity", 4 },
+ { "minLength", 128 },
{ "requireUpper", true },
{ "requireLower", false },
{ "requireNumbers", true },
@@ -397,4 +397,48 @@ public async Task PutVNext_PolicyWithNullData_Success()
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
+
+ [Fact]
+ public async Task Put_MasterPasswordPolicy_ExcessiveMinLength_ReturnsBadRequest()
+ {
+ // Arrange
+ var policyType = PolicyType.MasterPassword;
+ var request = new PolicyRequestModel
+ {
+ Enabled = true,
+ Data = new Dictionary
+ {
+ { "minLength", 129 }
+ }
+ };
+
+ // Act
+ var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}",
+ JsonContent.Create(request));
+
+ // Assert
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task Put_MasterPasswordPolicy_ExcessiveMinComplexity_ReturnsBadRequest()
+ {
+ // Arrange
+ var policyType = PolicyType.MasterPassword;
+ var request = new PolicyRequestModel
+ {
+ Enabled = true,
+ Data = new Dictionary
+ {
+ { "minComplexity", 5 }
+ }
+ };
+
+ // Act
+ var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}",
+ JsonContent.Create(request));
+
+ // Assert
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
}
diff --git a/test/Api.IntegrationTest/AdminConsole/Public/Controllers/PoliciesControllerTests.cs b/test/Api.IntegrationTest/AdminConsole/Public/Controllers/PoliciesControllerTests.cs
index 6144d7eebb1a..a669bdd93cfa 100644
--- a/test/Api.IntegrationTest/AdminConsole/Public/Controllers/PoliciesControllerTests.cs
+++ b/test/Api.IntegrationTest/AdminConsole/Public/Controllers/PoliciesControllerTests.cs
@@ -61,7 +61,8 @@ public async Task Post_NewPolicy()
Enabled = true,
Data = new Dictionary
{
- { "minComplexity", 15},
+ { "minComplexity", 4},
+ { "minLength", 128 },
{ "requireLower", true}
}
};
@@ -78,7 +79,8 @@ public async Task Post_NewPolicy()
Assert.IsType(result.Id);
Assert.NotEqual(default, result.Id);
Assert.NotNull(result.Data);
- Assert.Equal(15, ((JsonElement)result.Data["minComplexity"]).GetInt32());
+ Assert.Equal(4, ((JsonElement)result.Data["minComplexity"]).GetInt32());
+ Assert.Equal(128, ((JsonElement)result.Data["minLength"]).GetInt32());
Assert.True(((JsonElement)result.Data["requireLower"]).GetBoolean());
// Assert against the database values
@@ -94,7 +96,7 @@ public async Task Post_NewPolicy()
Assert.NotNull(policy.Data);
var data = policy.GetDataModel();
- var expectedData = new MasterPasswordPolicyData { MinComplexity = 15, RequireLower = true };
+ var expectedData = new MasterPasswordPolicyData { MinComplexity = 4, MinLength = 128, RequireLower = true };
AssertHelper.AssertPropertyEqual(expectedData, data);
}
@@ -242,4 +244,46 @@ public async Task Put_PolicyWithNullData_Success()
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
+
+ [Fact]
+ public async Task Put_MasterPasswordPolicy_ExcessiveMinLength_ReturnsBadRequest()
+ {
+ // Arrange
+ var policyType = PolicyType.MasterPassword;
+ var request = new PolicyUpdateRequestModel
+ {
+ Enabled = true,
+ Data = new Dictionary
+ {
+ { "minLength", 129 }
+ }
+ };
+
+ // Act
+ var response = await _client.PutAsync($"/public/policies/{policyType}", JsonContent.Create(request));
+
+ // Assert
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task Put_MasterPasswordPolicy_ExcessiveMinComplexity_ReturnsBadRequest()
+ {
+ // Arrange
+ var policyType = PolicyType.MasterPassword;
+ var request = new PolicyUpdateRequestModel
+ {
+ Enabled = true,
+ Data = new Dictionary
+ {
+ { "minComplexity", 5 }
+ }
+ };
+
+ // Act
+ var response = await _client.PutAsync($"/public/policies/{policyType}", JsonContent.Create(request));
+
+ // Assert
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
}
diff --git a/test/Core.Test/AdminConsole/Utilities/PolicyDataValidatorTests.cs b/test/Core.Test/AdminConsole/Utilities/PolicyDataValidatorTests.cs
index 43725d23e081..dcc4ceb2460b 100644
--- a/test/Core.Test/AdminConsole/Utilities/PolicyDataValidatorTests.cs
+++ b/test/Core.Test/AdminConsole/Utilities/PolicyDataValidatorTests.cs
@@ -19,12 +19,17 @@ public void ValidateAndSerialize_NullData_ReturnsNull()
[Fact]
public void ValidateAndSerialize_ValidData_ReturnsSerializedJson()
{
- var data = new Dictionary { { "minLength", 12 } };
+ var data = new Dictionary
+ {
+ { "minLength", 12 },
+ { "minComplexity", 4 }
+ };
var result = PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword);
Assert.NotNull(result);
Assert.Contains("\"minLength\":12", result);
+ Assert.Contains("\"minComplexity\":4", result);
}
[Fact]
@@ -56,4 +61,122 @@ public void ValidateAndDeserializeMetadata_ValidMetadata_ReturnsModel()
Assert.IsType(result);
}
+
+ [Fact]
+ public void ValidateAndSerialize_ExcessiveMinLength_ThrowsBadRequestException()
+ {
+ var data = new Dictionary { { "minLength", 129 } };
+
+ var exception = Assert.Throws(() =>
+ PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));
+
+ Assert.Contains("Invalid data for MasterPassword policy", exception.Message);
+ }
+
+ [Fact]
+ public void ValidateAndSerialize_ExcessiveMinComplexity_ThrowsBadRequestException()
+ {
+ var data = new Dictionary { { "minComplexity", 5 } };
+
+ var exception = Assert.Throws(() =>
+ PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));
+
+ Assert.Contains("Invalid data for MasterPassword policy", exception.Message);
+ }
+
+ [Fact]
+ public void ValidateAndSerialize_MinLengthAtMinimum_Succeeds()
+ {
+ var data = new Dictionary { { "minLength", 12 } };
+
+ var result = PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword);
+
+ Assert.NotNull(result);
+ Assert.Contains("\"minLength\":12", result);
+ }
+
+ [Fact]
+ public void ValidateAndSerialize_MinLengthAtMaximum_Succeeds()
+ {
+ var data = new Dictionary { { "minLength", 128 } };
+
+ var result = PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword);
+
+ Assert.NotNull(result);
+ Assert.Contains("\"minLength\":128", result);
+ }
+
+ [Fact]
+ public void ValidateAndSerialize_MinLengthBelowMinimum_ThrowsBadRequestException()
+ {
+ var data = new Dictionary { { "minLength", 11 } };
+
+ var exception = Assert.Throws(() =>
+ PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));
+
+ Assert.Contains("Invalid data for MasterPassword policy", exception.Message);
+ }
+
+ [Fact]
+ public void ValidateAndSerialize_MinComplexityAtMinimum_Succeeds()
+ {
+ var data = new Dictionary { { "minComplexity", 0 } };
+
+ var result = PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword);
+
+ Assert.NotNull(result);
+ Assert.Contains("\"minComplexity\":0", result);
+ }
+
+ [Fact]
+ public void ValidateAndSerialize_MinComplexityAtMaximum_Succeeds()
+ {
+ var data = new Dictionary { { "minComplexity", 4 } };
+
+ var result = PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword);
+
+ Assert.NotNull(result);
+ Assert.Contains("\"minComplexity\":4", result);
+ }
+
+ [Fact]
+ public void ValidateAndSerialize_MinComplexityBelowMinimum_ThrowsBadRequestException()
+ {
+ var data = new Dictionary { { "minComplexity", -1 } };
+
+ var exception = Assert.Throws(() =>
+ PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));
+
+ Assert.Contains("Invalid data for MasterPassword policy", exception.Message);
+ }
+
+ [Fact]
+ public void ValidateAndSerialize_NullMinLength_Succeeds()
+ {
+ var data = new Dictionary
+ {
+ { "minComplexity", 2 }
+ // minLength is omitted, should be null
+ };
+
+ var result = PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword);
+
+ Assert.NotNull(result);
+ Assert.Contains("\"minComplexity\":2", result);
+ }
+
+ [Fact]
+ public void ValidateAndSerialize_MultipleInvalidFields_ThrowsBadRequestException()
+ {
+ var data = new Dictionary
+ {
+ { "minLength", 200 },
+ { "minComplexity", 10 }
+ };
+
+ var exception = Assert.Throws(() =>
+ PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));
+
+ Assert.Contains("Invalid data for MasterPassword policy", exception.Message);
+ }
}