1
0
mirror of https://github.com/bitwarden/server synced 2026-01-28 23:36:12 +00:00

Merge branch 'main' into billing/pm-30908/correct-premium-subscription-status-handling

This commit is contained in:
cyprain-okeke
2026-01-26 15:32:26 +01:00
committed by GitHub
5 changed files with 246 additions and 10 deletions

View File

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

View File

@@ -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<MasterPasswordPolicyData>(json);
var masterPasswordData = CoreHelpers.LoadClassFromJsonData<MasterPasswordPolicyData>(json);
ValidateModel(masterPasswordData, policyType);
break;
case PolicyType.SendOptions:
CoreHelpers.LoadClassFromJsonData<SendOptionsPolicyData>(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<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>

View File

@@ -150,8 +150,8 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
Enabled = true,
Data = new Dictionary<string, object>
{
{ "minComplexity", 10 },
{ "minLength", 12 },
{ "minComplexity", 4 },
{ "minLength", 128 },
{ "requireUpper", true },
{ "requireLower", false },
{ "requireNumbers", true },
@@ -397,4 +397,48 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
// 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);
}
}

View File

@@ -61,7 +61,8 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
Enabled = true,
Data = new Dictionary<string, object>
{
{ "minComplexity", 15},
{ "minComplexity", 4},
{ "minLength", 128 },
{ "requireLower", true}
}
};
@@ -78,7 +79,8 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
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
@@ -94,7 +96,7 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
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);
}
@@ -242,4 +244,46 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
// 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);
}
}

View File

@@ -19,12 +19,17 @@ public class PolicyDataValidatorTests
[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]
@@ -56,4 +61,122 @@ public class PolicyDataValidatorTests
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);
}
}