using Microsoft.Extensions.Options; using ZB.MOM.WW.OtOpcUa.Configuration.Services; namespace ZB.MOM.WW.OtOpcUa.Admin.Security; /// /// Default . Merges the static appsettings.json /// bootstrap dictionary with the DB-backed LdapGroupRoleMapping rows. See /// for the scope split and the decision-#150 control-plane note. /// public sealed class AdminRoleGrantResolver( ILdapGroupRoleMappingService mappingService, IOptions ldapOptions) : IAdminRoleGrantResolver { private readonly LdapOptions _ldap = ldapOptions.Value; public async Task ResolveAsync( IReadOnlyList ldapGroups, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(ldapGroups); if (ldapGroups.Count == 0) return AdminRoleGrants.Empty; // Static bootstrap dictionary — always fleet-wide, lock-out-proof fallback. var fleet = new HashSet( RoleMapper.Map(ldapGroups, _ldap.GroupToRole), StringComparer.OrdinalIgnoreCase); // DB-backed grants stack additively. A system-wide row folds into the fleet set; // a cluster-scoped row becomes a (cluster, role) grant, deduped on that pair. var mappings = await mappingService.GetByGroupsAsync(ldapGroups, cancellationToken) .ConfigureAwait(false); var cluster = new Dictionary<(string, string), ClusterRoleGrant>(); foreach (var m in mappings) { var roleName = m.Role.ToString(); if (m.IsSystemWide || string.IsNullOrEmpty(m.ClusterId)) { fleet.Add(roleName); } else { var key = (m.ClusterId, roleName); cluster[key] = new ClusterRoleGrant(m.ClusterId, roleName); } } return new AdminRoleGrants([.. fleet], [.. cluster.Values]); } }