Stream A.1-A.2 per docs/v2/implementation/phase-6-2-authorization-runtime.md.
Seed-data migration (A.3) is a separate follow-up once production LDAP group
DNs are finalised; until then CRUD via the Admin UI handles the fleet set up.
Configuration:
- New AdminRole enum {ConfigViewer, ConfigEditor, FleetAdmin} — string-stored.
- New LdapGroupRoleMapping entity with Id (surrogate PK), LdapGroup (512 chars),
Role (AdminRole enum), ClusterId (nullable, FK to ServerCluster), IsSystemWide,
CreatedAtUtc, Notes.
- EF config: UX_LdapGroupRoleMapping_Group_Cluster unique index on
(LdapGroup, ClusterId) + IX_LdapGroupRoleMapping_Group hot-path index on
LdapGroup for sign-in lookups. Cluster FK cascades on cluster delete.
- Migration 20260419_..._AddLdapGroupRoleMapping generated via `dotnet ef`.
Configuration.Services:
- ILdapGroupRoleMappingService — CRUD surface. Declared as control-plane only
per decision #150; the OPC UA data-path evaluator must NOT depend on this
interface (Phase 6.2 compliance check on control/data-plane separation).
GetByGroupsAsync is the hot-path sign-in lookup.
- LdapGroupRoleMappingService (EF Core impl) enforces the write-time invariant
"exactly one of (ClusterId populated, IsSystemWide=true)" and surfaces
InvalidLdapGroupRoleMappingException on violation. Create auto-populates Id
+ CreatedAtUtc when omitted.
Tests (9 new, all pass) in Configuration.Tests:
- Create sets Id + CreatedAtUtc.
- Create rejects empty LdapGroup.
- Create rejects IsSystemWide=true with populated ClusterId.
- Create rejects IsSystemWide=false with null ClusterId.
- GetByGroupsAsync returns matching rows only.
- GetByGroupsAsync with empty input returns empty (no full-table scan).
- ListAllAsync orders by group then cluster.
- Delete removes the target row.
- Delete of unknown id is a no-op.
Microsoft.EntityFrameworkCore.InMemory 10.0.0 added to Configuration.Tests for
the service-level tests (schema-compliance tests still use the live SQL
fixture).
SchemaComplianceTests updated to expect the new LdapGroupRoleMapping table.
Full solution dotnet test: 1051 passing (baseline 906, Phase 6.1 shipped at
1042, Phase 6.2 Stream A adds 9 = 1051). Pre-existing Client.CLI Subscribe
flake unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
48 lines
2.4 KiB
C#
48 lines
2.4 KiB
C#
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) { }
|
|
}
|