Apply technical-light design system to the gateway dashboard
Restyles the Blazor dashboard onto a portable token-based theme so it reads like an instrument panel: warm-paper background, hairline-ruled panels, IBM Plex type, monospace tabular numerics, and status carried by colour chips. Vendors theme.css + IBM Plex fonts, rewrites dashboard.css as a thin token-driven view layer, and swaps the Bootstrap navbar and status badges for the design-system app bar and chips. Also includes pending API-key management, Galaxy hierarchy projection, and constraint-enforcement work with their tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
using MxGateway.Contracts.Proto.Galaxy;
|
||||
using MxGateway.Server.Galaxy;
|
||||
using MxGateway.Server.Security.Authentication;
|
||||
using MxGateway.Server.Sessions;
|
||||
|
||||
namespace MxGateway.Server.Security.Authorization;
|
||||
|
||||
public sealed class ConstraintEnforcer(
|
||||
IGalaxyHierarchyCache cache,
|
||||
IApiKeyAuditStore auditStore) : IConstraintEnforcer
|
||||
{
|
||||
public Task<ConstraintFailure?> CheckReadTagAsync(
|
||||
ApiKeyIdentity? identity,
|
||||
string tagAddress,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ApiKeyConstraints constraints = identity?.EffectiveConstraints ?? ApiKeyConstraints.Empty;
|
||||
if (!constraints.HasReadConstraints)
|
||||
{
|
||||
return Task.FromResult<ConstraintFailure?>(null);
|
||||
}
|
||||
|
||||
return Task.FromResult(CheckReadTarget(constraints, tagAddress));
|
||||
}
|
||||
|
||||
public Task<ConstraintFailure?> CheckReadHandleAsync(
|
||||
ApiKeyIdentity? identity,
|
||||
GatewaySession session,
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ApiKeyConstraints constraints = identity?.EffectiveConstraints ?? ApiKeyConstraints.Empty;
|
||||
if (!constraints.HasReadConstraints)
|
||||
{
|
||||
return Task.FromResult<ConstraintFailure?>(null);
|
||||
}
|
||||
|
||||
if (!session.TryGetItemRegistration(serverHandle, itemHandle, out SessionItemRegistration registration))
|
||||
{
|
||||
return Task.FromResult<ConstraintFailure?>(new ConstraintFailure("item_handle", "Item handle is not registered in the constrained session."));
|
||||
}
|
||||
|
||||
return Task.FromResult(CheckReadTarget(constraints, registration.TagAddress));
|
||||
}
|
||||
|
||||
public Task<ConstraintFailure?> CheckWriteHandleAsync(
|
||||
ApiKeyIdentity? identity,
|
||||
GatewaySession session,
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ApiKeyConstraints constraints = identity?.EffectiveConstraints ?? ApiKeyConstraints.Empty;
|
||||
if (!constraints.HasWriteConstraints)
|
||||
{
|
||||
return Task.FromResult<ConstraintFailure?>(null);
|
||||
}
|
||||
|
||||
if (!session.TryGetItemRegistration(serverHandle, itemHandle, out SessionItemRegistration registration))
|
||||
{
|
||||
return Task.FromResult<ConstraintFailure?>(new ConstraintFailure("item_handle", "Item handle is not registered in the constrained session."));
|
||||
}
|
||||
|
||||
GalaxyTagLookup? target = ResolveTarget(registration.TagAddress);
|
||||
if (target is null)
|
||||
{
|
||||
return Task.FromResult<ConstraintFailure?>(new ConstraintFailure("tag_metadata", "Tag metadata is not available in the Galaxy hierarchy cache."));
|
||||
}
|
||||
|
||||
if (!MatchesPathOrTag(target.ContainedPath, registration.TagAddress, constraints.WriteSubtrees, constraints.WriteTagGlobs))
|
||||
{
|
||||
return Task.FromResult<ConstraintFailure?>(new ConstraintFailure("write_scope", "Tag is outside the API key write scope."));
|
||||
}
|
||||
|
||||
if (constraints.MaxWriteClassification is { } maxClassification)
|
||||
{
|
||||
GalaxyAttribute? attribute = target.Attribute;
|
||||
if (attribute is null)
|
||||
{
|
||||
return Task.FromResult<ConstraintFailure?>(new ConstraintFailure("max_write_classification", "Attribute security classification is not available."));
|
||||
}
|
||||
|
||||
if (attribute.SecurityClassification > maxClassification)
|
||||
{
|
||||
return Task.FromResult<ConstraintFailure?>(new ConstraintFailure(
|
||||
"max_write_classification",
|
||||
$"Attribute security classification {attribute.SecurityClassification} exceeds allowed maximum {maxClassification}."));
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult<ConstraintFailure?>(null);
|
||||
}
|
||||
|
||||
public async Task RecordDenialAsync(
|
||||
ApiKeyIdentity? identity,
|
||||
string commandKind,
|
||||
string target,
|
||||
ConstraintFailure failure,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await auditStore.AppendAsync(
|
||||
new ApiKeyAuditEntry(
|
||||
KeyId: identity?.KeyId,
|
||||
EventType: "constraint-denied",
|
||||
RemoteAddress: null,
|
||||
Details: $"{commandKind}: {target}: {failure.ConstraintName}: {failure.Message}"),
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private ConstraintFailure? CheckReadTarget(
|
||||
ApiKeyConstraints constraints,
|
||||
string tagAddress)
|
||||
{
|
||||
GalaxyTagLookup? target = ResolveTarget(tagAddress);
|
||||
if (target is null)
|
||||
{
|
||||
return new ConstraintFailure("tag_metadata", "Tag metadata is not available in the Galaxy hierarchy cache.");
|
||||
}
|
||||
|
||||
if (!MatchesPathOrTag(target.ContainedPath, tagAddress, constraints.ReadSubtrees, constraints.ReadTagGlobs))
|
||||
{
|
||||
return new ConstraintFailure("read_scope", "Tag is outside the API key read scope.");
|
||||
}
|
||||
|
||||
if (constraints.ReadAlarmOnly && target.Attribute is not { IsAlarm: true })
|
||||
{
|
||||
return new ConstraintFailure("read_alarm_only", "Tag is not an alarm-bearing attribute.");
|
||||
}
|
||||
|
||||
if (constraints.ReadHistorizedOnly && target.Attribute is not { IsHistorized: true })
|
||||
{
|
||||
return new ConstraintFailure("read_historized_only", "Tag is not a historized attribute.");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private GalaxyTagLookup? ResolveTarget(string tagAddress)
|
||||
{
|
||||
GalaxyHierarchyCacheEntry entry = cache.Current;
|
||||
return !string.IsNullOrWhiteSpace(tagAddress)
|
||||
&& entry.Index.TagsByAddress.TryGetValue(tagAddress, out GalaxyTagLookup? lookup)
|
||||
? lookup
|
||||
: null;
|
||||
}
|
||||
|
||||
private static bool MatchesPathOrTag(
|
||||
string containedPath,
|
||||
string tagAddress,
|
||||
IReadOnlyList<string> subtreeGlobs,
|
||||
IReadOnlyList<string> tagGlobs)
|
||||
{
|
||||
bool hasSubtreeConstraint = subtreeGlobs.Count > 0;
|
||||
bool hasTagConstraint = tagGlobs.Count > 0;
|
||||
if (!hasSubtreeConstraint && !hasTagConstraint)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return subtreeGlobs.Any(glob => GalaxyGlobMatcher.IsMatch(containedPath, glob))
|
||||
|| tagGlobs.Any(glob => GalaxyGlobMatcher.IsMatch(tagAddress, glob));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace MxGateway.Server.Security.Authorization;
|
||||
|
||||
public sealed record ConstraintFailure(string ConstraintName, string Message);
|
||||
@@ -0,0 +1,33 @@
|
||||
using MxGateway.Server.Security.Authentication;
|
||||
using MxGateway.Server.Sessions;
|
||||
|
||||
namespace MxGateway.Server.Security.Authorization;
|
||||
|
||||
public interface IConstraintEnforcer
|
||||
{
|
||||
Task<ConstraintFailure?> CheckReadTagAsync(
|
||||
ApiKeyIdentity? identity,
|
||||
string tagAddress,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<ConstraintFailure?> CheckReadHandleAsync(
|
||||
ApiKeyIdentity? identity,
|
||||
GatewaySession session,
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<ConstraintFailure?> CheckWriteHandleAsync(
|
||||
ApiKeyIdentity? identity,
|
||||
GatewaySession session,
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task RecordDenialAsync(
|
||||
ApiKeyIdentity? identity,
|
||||
string commandKind,
|
||||
string target,
|
||||
ConstraintFailure failure,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
Reference in New Issue
Block a user