Skip to content
Merged
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
Expand Up @@ -13,7 +13,7 @@ namespace ServiceControl.AcceptanceTests.Security.OpenIdConnect;
using ServiceControl.Infrastructure.Auth;

/// <summary>
/// my/routes returns the API routes the current token may call, as { method, urlTemplate } entries.
/// my/routes returns the API routes the current token may call, as { method, url_template } entries.
/// It is the per-instance authorization contract ServicePulse consumes: it gates UI on routes it
/// already calls rather than on the server's internal permission vocabulary.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/ServiceControl.Hosting/Auth/MyRoutesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace ServiceControl.Hosting.Auth;
using ServiceControl.Infrastructure.Auth;

/// <summary>
/// Returns the API routes the current token may call, as <c>{ method, urlTemplate }</c> entries.
/// Returns the API routes the current token may call, as <c>{ method, url_template }</c> entries.
/// This is the per-instance authorization contract for clients (ServicePulse): each instance reports
/// only the routes it serves, so a client matches its outgoing request against the allowed set without
/// ever learning the server's internal permission vocabulary. The endpoint is the bootstrap of that
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#nullable enable
namespace ServiceControl.Infrastructure.Tests.Auth;

using System.Text.Json;
using NUnit.Framework;
using ServiceControl.Infrastructure.Auth;

[TestFixture]
class RouteManifestEntrySerializationTests
{
// The my/routes manifest must have ONE wire shape across instances. The Primary instance
// serializes snake_case and the Monitoring instance camelCase, so RouteManifestEntry pins its
// field names with [JsonPropertyName]. This test guards that contract: even under a camelCase
// policy (as on the Monitoring host) the emitted names stay snake_case, so a client merging both
// instances never silently drops the differently-cased entries.
[Test]
public void Emits_snake_case_field_names_even_under_a_camelCase_policy()
{
var json = JsonSerializer.Serialize(
new RouteManifestEntry("GET", "/api/errors"),
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });

Assert.That(json, Does.Contain("\"method\""));
Assert.That(json, Does.Contain("\"url_template\""));
Assert.That(json, Does.Not.Contain("urlTemplate"));
}
}
13 changes: 11 additions & 2 deletions src/ServiceControl.Infrastructure/Auth/RouteManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace ServiceControl.Infrastructure.Auth;

using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

/// <summary>
Expand All @@ -28,8 +29,16 @@ public static string Normalize(string rawTemplate)
/// <summary>A route the server hosts, with the authorization metadata read from its endpoint.</summary>
public sealed record RouteAuthInfo(string Method, string UrlTemplate, string? RequiredPermission, bool AllowAnonymous);

/// <summary>A single allowed-route entry returned to the client.</summary>
public sealed record RouteManifestEntry(string Method, string UrlTemplate);
/// <summary>
/// A single allowed-route entry returned to the client. The JSON field names are pinned with
/// <see cref="JsonPropertyName"/> so the manifest has one stable shape regardless of each host's
/// global JSON naming policy (the Primary instance serializes snake_case, the Monitoring instance
/// camelCase). Without this the same contract would emit <c>url_template</c> on one instance and
/// <c>urlTemplate</c> on another, and clients that merge both would silently drop half the routes.
/// </summary>
public sealed record RouteManifestEntry(
[property: JsonPropertyName("method")] string Method,
[property: JsonPropertyName("url_template")] string UrlTemplate);

/// <summary>
/// Projects the route table down to the entries a caller may invoke. A route is included when it is
Expand Down
Loading