rename: prefix gateway projects/namespaces with ZB.MOM.WW + sln→slnx
Apply the ZB.MOM.WW. prefix to all gateway-side projects, folders,
.csproj/.sln contents, C# namespaces, using directives, generated proto
C# (csharp_namespace + checked-in generated files), InternalsVisibleTo
attributes, project-name string literals (LoadProject, .sln lookups,
worker exe paths, staticwebassets manifest), and the install/script/doc
references that point at any of the above. Migrate the solution from
.sln to .slnx via `dotnet sln migrate` and delete the old file.
External-runtime identifiers are intentionally NOT prefixed so external
configuration keeps working:
- GatewayMetrics.cs MeterName ("MxGateway.Server")
- DashboardAuthenticationDefaults Scheme/Policy ("MxGateway.Dashboard")
- GatewayRequestLoggingMiddleware logger category ("MxGateway.Request")
- StaRuntime thread name ("MxGateway.Worker.STA")
- appsettings.json root section "MxGateway" + env-var prefix
MxGateway__... and secret-name MxGateway:ApiKeyPepper
- C:\ProgramData\MxGateway\ data dir paths
Also fixes two tests that were not rename-related but became visible
while validating the rename:
- WorkerLiveMxAccessSmokeTests.ShutDownAsync: cancellation that the
gateway service correctly maps to RpcException(Cancelled) per gRPC
convention was being misclassified as a stream fault. Added a sibling
catch on RpcException with StatusCode.Cancelled.
- IntegrationTestEnvironment.ResolveRepositoryRoot: extracted IsRepositoryRoot
and made it accept either a .git marker OR a .sln/.slnx next to src/
so the worker-exe walker works in non-git working copies.
clients/proto/proto-inputs.json's protoRoot updated to point at
src/ZB.MOM.WW.MxGateway.Contracts/Protos.
Verified by `dotnet build` and a full `dotnet test` of the .slnx with
MXGATEWAY_RUN_LIVE_{MXACCESS,LDAP,GALAXY}_TESTS=1:
Tests: 472/472 pass
Worker.Tests: 280/280 pass (4 dev-rig [Fact(Skip=...)] skipped)
IntegrationTests: 18/18 pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
||||
using ZB.MOM.WW.MxGateway.Server.Galaxy;
|
||||
using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
|
||||
using ZB.MOM.WW.MxGateway.Server.Sessions;
|
||||
|
||||
namespace ZB.MOM.WW.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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user