From 1869511c19f1afb8975e08b3a9f15e9ba8142adb Mon Sep 17 00:00:00 2001 From: ahall Date: Mon, 15 Jun 2026 10:11:31 +0100 Subject: [PATCH] fix: validate UnauthorizedStatusCode is in the 4xx range (#447) UnauthorizedStatusCode previously accepted any integer, allowing a misconfiguration to silently turn authorization failures into success responses. Add a backing field with range validation (400-499) following the existing MaxTop/PageSize pattern, throwing ArgumentOutOfRangeException for out-of-range values. Update tests and docs accordingly. --- docs/in-depth/server/index.md | 2 +- .../Models/TableControllerOptions.cs | 17 ++++++++++- .../Models/TableControllerOptions_Tests.cs | 28 +++++++++++++++++-- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/docs/in-depth/server/index.md b/docs/in-depth/server/index.md index d5210d91..c36e2f4b 100644 --- a/docs/in-depth/server/index.md +++ b/docs/in-depth/server/index.md @@ -130,7 +130,7 @@ The options you can set include: * `PageSize` (int, default: 100) is the maximum number of items a query operation returns in a single page. * `MaxTop` (int, default: 512000) is the maximum number of items a user can request in a single operation. * `EnableSoftDelete` (bool, default: false) enables soft-delete, which marks items as deleted instead of deleting them from the database. Soft delete allows clients to update their offline cache, but requires that deleted items are purged from the database separately. -* `UnauthorizedStatusCode` (int, default: 401 Unauthorized) is the status code returned when the user isn't allowed to do an action. +* `UnauthorizedStatusCode` (int, default: 401 Unauthorized) is the status code returned when the user isn't allowed to do an action. The value must be a client error (4xx) status code in the range 400-499. ## Configure access permissions diff --git a/src/CommunityToolkit.Datasync.Server/Models/TableControllerOptions.cs b/src/CommunityToolkit.Datasync.Server/Models/TableControllerOptions.cs index ca23c1e7..f6435b2f 100644 --- a/src/CommunityToolkit.Datasync.Server/Models/TableControllerOptions.cs +++ b/src/CommunityToolkit.Datasync.Server/Models/TableControllerOptions.cs @@ -13,6 +13,7 @@ public class TableControllerOptions { private int _pageSize = 100; private int _maxTop = MAX_TOP; + private int _unauthorizedStatusCode = StatusCodes.Status401Unauthorized; /// /// The maximum page size that can be specified by the server. @@ -73,5 +74,19 @@ public int PageSize /// /// The status code returned when the user is not authorized to perform an operation. /// - public int UnauthorizedStatusCode { get; set; } = StatusCodes.Status401Unauthorized; + /// + /// The value must be a client error (4xx) status code in the range 400-499. Setting a value + /// outside this range (for example, a success or server error code) is considered a + /// misconfiguration and throws . + /// + public int UnauthorizedStatusCode + { + get => this._unauthorizedStatusCode; + set + { + ArgumentOutOfRangeException.ThrowIfLessThan(value, 400, nameof(UnauthorizedStatusCode)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(value, 499, nameof(UnauthorizedStatusCode)); + this._unauthorizedStatusCode = value; + } + } } diff --git a/tests/CommunityToolkit.Datasync.Server.Test/Models/TableControllerOptions_Tests.cs b/tests/CommunityToolkit.Datasync.Server.Test/Models/TableControllerOptions_Tests.cs index 1fa1fc21..00d57587 100644 --- a/tests/CommunityToolkit.Datasync.Server.Test/Models/TableControllerOptions_Tests.cs +++ b/tests/CommunityToolkit.Datasync.Server.Test/Models/TableControllerOptions_Tests.cs @@ -33,14 +33,38 @@ public void Ctor_NoNegativeNumbers(int pageSize, int maxTop) act.Should().Throw(); } + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(200)] + [InlineData(399)] + [InlineData(500)] + [InlineData(510)] + public void Ctor_InvalidUnauthorizedStatusCode_Throws(int statusCode) + { + Action act = () => _ = new TableControllerOptions { UnauthorizedStatusCode = statusCode }; + act.Should().Throw(); + } + + [Theory] + [InlineData(400)] + [InlineData(401)] + [InlineData(403)] + [InlineData(499)] + public void Ctor_ValidUnauthorizedStatusCode_Roundtrips(int statusCode) + { + TableControllerOptions sut = new() { UnauthorizedStatusCode = statusCode }; + sut.UnauthorizedStatusCode.Should().Be(statusCode); + } + [Fact] public void Ctor_Roundtrips() { - TableControllerOptions sut = new() { EnableSoftDelete = true, MaxTop = 100, PageSize = 50, UnauthorizedStatusCode = 510 }; + TableControllerOptions sut = new() { EnableSoftDelete = true, MaxTop = 100, PageSize = 50, UnauthorizedStatusCode = 403 }; sut.EnableSoftDelete.Should().BeTrue(); sut.MaxTop.Should().Be(100); sut.PageSize.Should().Be(50); - sut.UnauthorizedStatusCode.Should().Be(510); + sut.UnauthorizedStatusCode.Should().Be(403); } }