Files
mxaccessgw/src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyConstraintSerializer.cs
T
Joseph Doherty a212e145ac feat(security): DashboardTags on API-key constraints; sessions inherit owner tags (SEC-25 groundwork)
Adds a dashboard event-visibility tag to ApiKeyConstraints, riding in the existing
constraints JSON blob so no auth-store schema migration is needed (design
docs/plans/2026-07-10-dashboard-session-acl-tst15.md sections 3/3.1, open call
settled per its own recommendation). The tag is visibility-only: no read, write,
browse, or subscribe path consults it, and HasRead/HasWriteConstraints ignore it.

GatewaySession gains an immutable, ordinal-ignore-case Tags set stamped at
construction from the owning API key, forwarded by MxAccessGatewayService.OpenSession
from the resolved ApiKeyIdentity — never from the wire request, so a client cannot
label its own session with another tenant's tag. ISessionManager gains a tag-carrying
OpenSessionAsync overload whose default implementation forwards to the tagless one, so
an implementation that does not model tags opens an untagged (least visible) session.

apikey create-key gains --dashboard-tags team-a,team-b (repeatable, trimmed,
de-duplicated; an empty segment is rejected rather than dropped) and list-keys prints
the tags column. No enforcement yet — the EventsHub ACL that consumes the tag is a
later change.
2026-08-17 03:58:14 -04:00

40 lines
1.7 KiB
C#

using System.Text.Json;
namespace ZB.MOM.WW.MxGateway.Server.Security.Authentication;
public static class ApiKeyConstraintSerializer
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
WriteIndented = false,
};
/// <summary>Serializes API key constraints to JSON, or returns null if the constraints are empty.</summary>
/// <param name="constraints">The constraints to serialize.</param>
/// <returns>The serialized JSON, or <see langword="null"/> when the constraints are empty.</returns>
public static string? Serialize(ApiKeyConstraints constraints)
{
ArgumentNullException.ThrowIfNull(constraints);
return constraints.IsEmpty ? null : JsonSerializer.Serialize(constraints, JsonOptions);
}
/// <summary>Deserializes API key constraints from JSON, or returns empty constraints if JSON is null or whitespace.</summary>
/// <param name="json">The JSON string to deserialize.</param>
/// <returns>The deserialized constraints, or <see cref="ApiKeyConstraints.Empty"/> when <paramref name="json"/> is null/whitespace.</returns>
/// <remarks>
/// Members absent from the JSON take their default: rows persisted before
/// <see cref="ApiKeyConstraints.DashboardTags"/> existed carry no <c>dashboard_tags</c>
/// member and deserialize to an untagged key, unchanged in every other respect.
/// </remarks>
public static ApiKeyConstraints Deserialize(string? json)
{
if (string.IsNullOrWhiteSpace(json))
{
return ApiKeyConstraints.Empty;
}
return JsonSerializer.Deserialize<ApiKeyConstraints>(json, JsonOptions) ?? ApiKeyConstraints.Empty;
}
}