diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Security/IInboundApiKeyAdmin.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Security/IInboundApiKeyAdmin.cs index 64b26b1d..ac1db4c1 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Security/IInboundApiKeyAdmin.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Security/IInboundApiKeyAdmin.cs @@ -34,6 +34,13 @@ public sealed record InboundApiKeyCreated(string KeyId, string Token); /// their method-scopes. The interface lives in Commons and is deliberately free of any /// dependency on the underlying auth library, so consumers depend only on this contract. /// +/// +/// Mutating operations (, , +/// , ) may throw on +/// store-level or configuration failures (e.g. an unavailable pepper) rather than +/// exclusively signalling failure via their bool return — callers must handle +/// exceptions in addition to checking the return value. +/// public interface IInboundApiKeyAdmin { /// Creates a new key scoped to and returns its @@ -58,9 +65,11 @@ public interface IInboundApiKeyAdmin Task DeleteAsync(string keyId, CancellationToken ct = default); /// Returns the method-scope set for a key, or an empty list if not found. + /// Enumerates the full key list (O(n)); intended for admin-scale use, not hot paths. Task> GetMethodsForKeyAsync(string keyId, CancellationToken ct = default); /// Returns the identifiers of all keys whose scopes contain /// . + /// Enumerates the full key list (O(n)); intended for admin-scale use, not hot paths. Task> GetKeysForMethodAsync(string methodName, CancellationToken ct = default); } diff --git a/src/ZB.MOM.WW.ScadaBridge.Security/LibraryInboundApiKeyAdmin.cs b/src/ZB.MOM.WW.ScadaBridge.Security/LibraryInboundApiKeyAdmin.cs index bfb02cb5..778ef99c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Security/LibraryInboundApiKeyAdmin.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Security/LibraryInboundApiKeyAdmin.cs @@ -34,8 +34,11 @@ public sealed class LibraryInboundApiKeyAdmin : IInboundApiKeyAdmin public async Task CreateAsync( string name, IReadOnlyCollection methods, CancellationToken ct = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(name); ArgumentNullException.ThrowIfNull(methods); + // "N" format = 32 hex chars, no hyphens/underscores — the library rejects underscores + // in keyId because they delimit the sbk__ token. var keyId = Guid.NewGuid().ToString("N"); var result = await _admin.CreateKeyAsync( keyId, name, methods.ToHashSet(StringComparer.Ordinal), diff --git a/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/LibraryInboundApiKeyAdminTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/LibraryInboundApiKeyAdminTests.cs index 31c8de26..2f277dba 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/LibraryInboundApiKeyAdminTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/LibraryInboundApiKeyAdminTests.cs @@ -54,6 +54,18 @@ public sealed class LibraryInboundApiKeyAdminTests : IAsyncLifetime _sut = new LibraryInboundApiKeyAdmin(commands); } + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task CreateAsync_NullOrWhitespaceName_Throws(string? name) + { + // ThrowIfNullOrWhiteSpace throws ArgumentNullException for null and ArgumentException + // for empty/whitespace; both are ArgumentException subtypes, so ThrowsAnyAsync covers all. + await Assert.ThrowsAnyAsync( + () => _sut.CreateAsync(name!, new[] { "MethodA" }, CancellationToken.None)); + } + [Fact] public async Task CreateAsync_ReturnsKeyIdAndToken_TokenStartsWith_sbk() { diff --git a/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/SecurityTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/SecurityTests.cs index 3eabd496..2fe3dcb3 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/SecurityTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/SecurityTests.cs @@ -443,6 +443,9 @@ public class SecurityReviewRegressionTests services.AddLogging(); services.AddDataProtection(); services.AddSecurity(); + // Explicitly set RequireHttpsCookie=true so the test asserts SecurePolicy.Always + // without relying on the SecurityOptions default value. + services.Configure(o => o.RequireHttpsCookie = true); using var provider = services.BuildServiceProvider(); var cookieOptions = provider