1
0
mirror of https://github.com/bitwarden/server synced 2026-01-30 16:23:37 +00:00
Files
server/test/Api.IntegrationTest/AdminConsole/Public/Controllers/PoliciesControllerTests.cs
Rui Tomé c8124667ee [PM-28842] Add validation to prevent excessive master password policy values (#6807)
* Enhance MasterPasswordPolicyData with validation attributes

Added data annotations for MinComplexity and MinLength properties to enforce validation rules. MinComplexity must be between 0 and 4, and MinLength must be between 12 and 128.

* Implement model validation in PolicyDataValidator and enhance error handling

Added a ValidateModel method to enforce validation rules for policy data. Updated error messages to provide clearer feedback on validation failures. Enhanced unit tests to cover new validation scenarios for MinLength and MinComplexity properties.

* Update PoliciesControllerTests to reflect new validation rules for MinComplexity and MinLength

Modified test cases to use updated values for MinComplexity (4) and MinLength (128). Added new tests to verify that excessive values for these properties return BadRequest responses. Ensured consistency across integration tests for both Admin and Public controllers.

* Enhance MasterPasswordPolicyData with XML documentation for properties

Added XML documentation comments for MinComplexity and MinLength properties to clarify their purpose and constraints. This improves code readability and provides better context for developers using the model.

* Add unit tests for PolicyDataValidator to validate minLength and minComplexity rules

Implemented new test cases to verify the behavior of the ValidateAndSerialize method in PolicyDataValidator. Tests cover scenarios for minimum and maximum values, as well as edge cases for invalid inputs, ensuring robust validation for MasterPassword policy data.
2026-01-26 11:38:06 +00:00

290 lines
9.6 KiB
C#

using System.Net;
using System.Text.Json;
using Bit.Api.AdminConsole.Public.Models.Request;
using Bit.Api.AdminConsole.Public.Models.Response;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Enums;
using Bit.Test.Common.Helpers;
using Xunit;
namespace Bit.Api.IntegrationTest.AdminConsole.Public.Controllers;
public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
{
private readonly HttpClient _client;
private readonly ApiApplicationFactory _factory;
private readonly LoginHelper _loginHelper;
// These will get set in `InitializeAsync` which is ran before all tests
private Organization _organization = null!;
private string _ownerEmail = null!;
public PoliciesControllerTests(ApiApplicationFactory factory)
{
_factory = factory;
_client = factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
}
public async Task InitializeAsync()
{
// Create the owner account
_ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(_ownerEmail);
// Create the organization
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
// Authorize with the organization api key
await _loginHelper.LoginWithOrganizationApiKeyAsync(_organization.Id);
}
public Task DisposeAsync()
{
_client.Dispose();
return Task.CompletedTask;
}
[Fact]
public async Task Post_NewPolicy()
{
var policyType = PolicyType.MasterPassword;
var request = new PolicyUpdateRequestModel
{
Enabled = true,
Data = new Dictionary<string, object>
{
{ "minComplexity", 4},
{ "minLength", 128 },
{ "requireLower", true}
}
};
var response = await _client.PutAsync($"/public/policies/{policyType}", JsonContent.Create(request));
// Assert against the response
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<PolicyResponseModel>();
Assert.NotNull(result);
Assert.True(result.Enabled);
Assert.Equal(policyType, result.Type);
Assert.IsType<Guid>(result.Id);
Assert.NotEqual(default, result.Id);
Assert.NotNull(result.Data);
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
var policyRepository = _factory.GetService<IPolicyRepository>();
var policy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType);
Assert.NotNull(policy);
Assert.True(policy.Enabled);
Assert.Equal(policyType, policy.Type);
Assert.IsType<Guid>(policy.Id);
Assert.NotEqual(default, policy.Id);
Assert.Equal(_organization.Id, policy.OrganizationId);
Assert.NotNull(policy.Data);
var data = policy.GetDataModel<MasterPasswordPolicyData>();
var expectedData = new MasterPasswordPolicyData { MinComplexity = 4, MinLength = 128, RequireLower = true };
AssertHelper.AssertPropertyEqual(expectedData, data);
}
[Fact]
public async Task Post_UpdatePolicy()
{
var policyType = PolicyType.MasterPassword;
var existingPolicy = new Policy
{
OrganizationId = _organization.Id,
Enabled = true,
Type = policyType
};
existingPolicy.SetDataModel(new MasterPasswordPolicyData
{
EnforceOnLogin = true,
MinLength = 22,
RequireSpecial = true
});
var policyRepository = _factory.GetService<IPolicyRepository>();
await policyRepository.UpsertAsync(existingPolicy);
// The Id isn't set until it's created in the database, get it back out to get the id
var createdPolicy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType);
var expectedId = createdPolicy!.Id;
var request = new PolicyUpdateRequestModel
{
Enabled = false,
Data = new Dictionary<string, object>
{
{ "minLength", 15},
{ "requireUpper", true}
}
};
var response = await _client.PutAsync($"/public/policies/{policyType}", JsonContent.Create(request));
// Assert against the response
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<PolicyResponseModel>();
Assert.NotNull(result);
Assert.False(result.Enabled);
Assert.Equal(policyType, result.Type);
Assert.Equal(expectedId, result.Id);
Assert.NotNull(result.Data);
Assert.Equal(15, ((JsonElement)result.Data["minLength"]).GetInt32());
Assert.True(((JsonElement)result.Data["requireUpper"]).GetBoolean());
// Assert against the database values
var policy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType);
Assert.NotNull(policy);
Assert.False(policy.Enabled);
Assert.Equal(policyType, policy.Type);
Assert.Equal(expectedId, policy.Id);
Assert.Equal(_organization.Id, policy.OrganizationId);
Assert.NotNull(policy.Data);
var data = policy.GetDataModel<MasterPasswordPolicyData>();
Assert.Equal(15, data.MinLength);
Assert.Equal(true, data.RequireUpper);
}
[Fact]
public async Task Put_MasterPasswordPolicy_InvalidDataType_ReturnsBadRequest()
{
// Arrange
var policyType = PolicyType.MasterPassword;
var request = new PolicyUpdateRequestModel
{
Enabled = true,
Data = new Dictionary<string, object>
{
{ "minLength", "not a number" }, // Wrong type - should be int
{ "requireUpper", true }
}
};
// Act
var response = await _client.PutAsync($"/public/policies/{policyType}", JsonContent.Create(request));
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task Put_SendOptionsPolicy_InvalidDataType_ReturnsBadRequest()
{
// Arrange
var policyType = PolicyType.SendOptions;
var request = new PolicyUpdateRequestModel
{
Enabled = true,
Data = new Dictionary<string, object>
{
{ "disableHideEmail", "not a boolean" } // Wrong type - should be bool
}
};
// Act
var response = await _client.PutAsync($"/public/policies/{policyType}", JsonContent.Create(request));
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task Put_ResetPasswordPolicy_InvalidDataType_ReturnsBadRequest()
{
// Arrange
var policyType = PolicyType.ResetPassword;
var request = new PolicyUpdateRequestModel
{
Enabled = true,
Data = new Dictionary<string, object>
{
{ "autoEnrollEnabled", 123 } // Wrong type - should be bool
}
};
// Act
var response = await _client.PutAsync($"/public/policies/{policyType}", JsonContent.Create(request));
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task Put_PolicyWithNullData_Success()
{
// Arrange
var policyType = PolicyType.DisableSend;
var request = new PolicyUpdateRequestModel
{
Enabled = true,
Data = null
};
// Act
var response = await _client.PutAsync($"/public/policies/{policyType}", JsonContent.Create(request));
// 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);
}
}