Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Minimum password complexity score (0-4). Null indicates no complexity requirement.
/// </summary>
[JsonPropertyName("minComplexity")]
[Range(0, 4)]
public int? MinComplexity { get; set; }

/// <summary>
/// Minimum password length (12-128). Null indicates no minimum length requirement.
/// </summary>
[JsonPropertyName("minLength")]
[Range(12, 128)]
public int? MinLength { get; set; }
[JsonPropertyName("requireLower")]
public bool? RequireLower { get; set; }
Expand Down
21 changes: 18 additions & 3 deletions src/Core/AdminConsole/Utilities/PolicyDataValidator.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -30,7 +31,8 @@ public static class PolicyDataValidator
switch (policyType)
{
case PolicyType.MasterPassword:
CoreHelpers.LoadClassFromJsonData<MasterPasswordPolicyData>(json);
var masterPasswordData = CoreHelpers.LoadClassFromJsonData<MasterPasswordPolicyData>(json);
ValidateModel(masterPasswordData, policyType);
break;
case PolicyType.SendOptions:
CoreHelpers.LoadClassFromJsonData<SendOptionsPolicyData>(json);
Expand All @@ -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<ValidationResult>();

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

/// <summary>
/// Validates and deserializes policy metadata based on the policy type.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ public async Task PutVNext_MasterPasswordPolicy_Success()
Enabled = true,
Data = new Dictionary<string, object>
{
{ "minComplexity", 10 },
{ "minLength", 12 },
{ "minComplexity", 4 },
{ "minLength", 128 },
{ "requireUpper", true },
{ "requireLower", false },
{ "requireNumbers", true },
Expand Down Expand Up @@ -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<string, object>
{
{ "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<string, object>
{
{ "minComplexity", 5 }
}
};

// Act
var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}",
JsonContent.Create(request));

// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ public async Task Post_NewPolicy()
Enabled = true,
Data = new Dictionary<string, object>
{
{ "minComplexity", 15},
{ "minComplexity", 4},
{ "minLength", 128 },
{ "requireLower", true}
}
};
Expand All @@ -78,7 +79,8 @@ public async Task Post_NewPolicy()
Assert.IsType<Guid>(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
Expand All @@ -94,7 +96,7 @@ public async Task Post_NewPolicy()

Assert.NotNull(policy.Data);
var data = policy.GetDataModel<MasterPasswordPolicyData>();
var expectedData = new MasterPasswordPolicyData { MinComplexity = 15, RequireLower = true };
var expectedData = new MasterPasswordPolicyData { MinComplexity = 4, MinLength = 128, RequireLower = true };
AssertHelper.AssertPropertyEqual(expectedData, data);
}

Expand Down Expand Up @@ -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<string, object>
{
{ "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<string, object>
{
{ "minComplexity", 5 }
}
};

// Act
var response = await _client.PutAsync($"/public/policies/{policyType}", JsonContent.Create(request));

// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
}
125 changes: 124 additions & 1 deletion test/Core.Test/AdminConsole/Utilities/PolicyDataValidatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,17 @@ public void ValidateAndSerialize_NullData_ReturnsNull()
[Fact]
public void ValidateAndSerialize_ValidData_ReturnsSerializedJson()
{
var data = new Dictionary<string, object> { { "minLength", 12 } };
var data = new Dictionary<string, object>
{
{ "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]
Expand Down Expand Up @@ -56,4 +61,122 @@ public void ValidateAndDeserializeMetadata_ValidMetadata_ReturnsModel()

Assert.IsType<OrganizationModelOwnershipPolicyModel>(result);
}

[Fact]
public void ValidateAndSerialize_ExcessiveMinLength_ThrowsBadRequestException()
{
var data = new Dictionary<string, object> { { "minLength", 129 } };

var exception = Assert.Throws<BadRequestException>(() =>
PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));

Assert.Contains("Invalid data for MasterPassword policy", exception.Message);
}

[Fact]
public void ValidateAndSerialize_ExcessiveMinComplexity_ThrowsBadRequestException()
{
var data = new Dictionary<string, object> { { "minComplexity", 5 } };

var exception = Assert.Throws<BadRequestException>(() =>
PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));

Assert.Contains("Invalid data for MasterPassword policy", exception.Message);
}

[Fact]
public void ValidateAndSerialize_MinLengthAtMinimum_Succeeds()
{
var data = new Dictionary<string, object> { { "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<string, object> { { "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<string, object> { { "minLength", 11 } };

var exception = Assert.Throws<BadRequestException>(() =>
PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));

Assert.Contains("Invalid data for MasterPassword policy", exception.Message);
}

[Fact]
public void ValidateAndSerialize_MinComplexityAtMinimum_Succeeds()
{
var data = new Dictionary<string, object> { { "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<string, object> { { "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<string, object> { { "minComplexity", -1 } };

var exception = Assert.Throws<BadRequestException>(() =>
PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));

Assert.Contains("Invalid data for MasterPassword policy", exception.Message);
}

[Fact]
public void ValidateAndSerialize_NullMinLength_Succeeds()
{
var data = new Dictionary<string, object>
{
{ "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<string, object>
{
{ "minLength", 200 },
{ "minComplexity", 10 }
};

var exception = Assert.Throws<BadRequestException>(() =>
PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));

Assert.Contains("Invalid data for MasterPassword policy", exception.Message);
}
}
Loading