chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)

Group all 69 projects into category subfolders under src/ and tests/ so the
Rider Solution Explorer mirrors the module structure. Folders: Core, Server,
Drivers (with a nested Driver CLIs subfolder), Client, Tooling.

- Move every project folder on disk with git mv (history preserved as renames).
- Recompute relative paths in 57 .csproj files: cross-category ProjectReferences,
  the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external
  mxaccessgw refs in Driver.Galaxy and its test project.
- Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders.
- Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL,
  integration, install).

Build green (0 errors); unit tests pass. Docs left for a separate pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-17 01:55:28 -04:00
parent 69f02fed7f
commit a25593a9c6
1044 changed files with 365 additions and 343 deletions
@@ -0,0 +1,47 @@
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.Configuration.Services;
/// <summary>
/// CRUD surface for <see cref="LdapGroupRoleMapping"/> — the control-plane mapping from
/// LDAP groups to Admin UI roles. Consumed only by Admin UI code paths; the OPC UA
/// data-path evaluator MUST NOT depend on this interface (see decision #150 and the
/// Phase 6.2 compliance check on control/data-plane separation).
/// </summary>
/// <remarks>
/// Per Phase 6.2 Stream A.2 this service is expected to run behind the Phase 6.1
/// <c>ResilientConfigReader</c> pipeline (timeout → retry → fallback-to-cache) so a
/// transient DB outage during sign-in falls back to the sealed snapshot rather than
/// denying every login.
/// </remarks>
public interface ILdapGroupRoleMappingService
{
/// <summary>List every mapping whose LDAP group matches one of <paramref name="ldapGroups"/>.</summary>
/// <remarks>
/// Hot path — fires on every sign-in. The default EF implementation relies on the
/// <c>IX_LdapGroupRoleMapping_Group</c> index. Case-insensitive per LDAP conventions.
/// </remarks>
Task<IReadOnlyList<LdapGroupRoleMapping>> GetByGroupsAsync(
IEnumerable<string> ldapGroups, CancellationToken cancellationToken);
/// <summary>Enumerate every mapping; Admin UI listing only.</summary>
Task<IReadOnlyList<LdapGroupRoleMapping>> ListAllAsync(CancellationToken cancellationToken);
/// <summary>Create a new grant.</summary>
/// <exception cref="InvalidLdapGroupRoleMappingException">
/// Thrown when the proposed row violates an invariant (IsSystemWide inconsistent with
/// ClusterId, duplicate (group, cluster) pair, etc.) — ValidatedLdapGroupRoleMappingService
/// is the write surface that enforces these; the raw service here surfaces DB-level violations.
/// </exception>
Task<LdapGroupRoleMapping> CreateAsync(LdapGroupRoleMapping row, CancellationToken cancellationToken);
/// <summary>Delete a mapping by its surrogate key.</summary>
Task DeleteAsync(Guid id, CancellationToken cancellationToken);
}
/// <summary>Thrown when <see cref="LdapGroupRoleMapping"/> authoring violates an invariant.</summary>
public sealed class InvalidLdapGroupRoleMappingException : Exception
{
public InvalidLdapGroupRoleMappingException(string message) : base(message) { }
}
@@ -0,0 +1,69 @@
using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
namespace ZB.MOM.WW.OtOpcUa.Configuration.Services;
/// <summary>
/// EF Core implementation of <see cref="ILdapGroupRoleMappingService"/>. Enforces the
/// "exactly one of (ClusterId, IsSystemWide)" invariant at the write surface so a
/// malformed row can't land in the DB.
/// </summary>
public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILdapGroupRoleMappingService
{
public async Task<IReadOnlyList<LdapGroupRoleMapping>> GetByGroupsAsync(
IEnumerable<string> ldapGroups, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(ldapGroups);
var groupSet = ldapGroups.ToList();
if (groupSet.Count == 0) return [];
return await db.LdapGroupRoleMappings
.AsNoTracking()
.Where(m => groupSet.Contains(m.LdapGroup))
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
}
public async Task<IReadOnlyList<LdapGroupRoleMapping>> ListAllAsync(CancellationToken cancellationToken)
=> await db.LdapGroupRoleMappings
.AsNoTracking()
.OrderBy(m => m.LdapGroup)
.ThenBy(m => m.ClusterId)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
public async Task<LdapGroupRoleMapping> CreateAsync(LdapGroupRoleMapping row, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(row);
ValidateInvariants(row);
if (row.Id == Guid.Empty) row.Id = Guid.NewGuid();
if (row.CreatedAtUtc == default) row.CreatedAtUtc = DateTime.UtcNow;
db.LdapGroupRoleMappings.Add(row);
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return row;
}
public async Task DeleteAsync(Guid id, CancellationToken cancellationToken)
{
var existing = await db.LdapGroupRoleMappings.FindAsync([id], cancellationToken).ConfigureAwait(false);
if (existing is null) return;
db.LdapGroupRoleMappings.Remove(existing);
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
private static void ValidateInvariants(LdapGroupRoleMapping row)
{
if (string.IsNullOrWhiteSpace(row.LdapGroup))
throw new InvalidLdapGroupRoleMappingException("LdapGroup must not be empty.");
if (row.IsSystemWide && !string.IsNullOrEmpty(row.ClusterId))
throw new InvalidLdapGroupRoleMappingException(
"IsSystemWide=true requires ClusterId to be null. A fleet-wide grant cannot also be cluster-scoped.");
if (!row.IsSystemWide && string.IsNullOrEmpty(row.ClusterId))
throw new InvalidLdapGroupRoleMappingException(
"IsSystemWide=false requires a populated ClusterId. A cluster-scoped grant needs its target cluster.");
}
}