using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; namespace ZB.MOM.WW.OtOpcUa.Core.Authorization; /// /// Builds a from a set of rows anchored /// in one generation. The trie is keyed on the rows' scope hierarchy — rows with /// land at the trie root, rows with /// land at a leaf, etc. /// /// /// Intended to be called by once per published /// generation; the resulting trie is immutable for the life of the cache entry. Idempotent — /// two builds from the same rows produce equal tries (grant lists may be in insertion order; /// evaluators don't depend on order). /// /// The builder deliberately does not know about the node-row metadata the trie path /// will be walked with. The caller assembles values from the live /// config (UnsArea parent of UnsLine, etc.); this class only honors the ScopeId /// each row carries. /// public static class PermissionTrieBuilder { /// /// Build a trie for one cluster/generation from the supplied rows. The caller is /// responsible for pre-filtering rows to the target generation + cluster. /// public static PermissionTrie Build( string clusterId, long generationId, IReadOnlyList rows, IReadOnlyDictionary? scopePaths = null) { ArgumentException.ThrowIfNullOrWhiteSpace(clusterId); ArgumentNullException.ThrowIfNull(rows); var trie = new PermissionTrie { ClusterId = clusterId, GenerationId = generationId }; foreach (var row in rows) { if (!string.Equals(row.ClusterId, clusterId, StringComparison.OrdinalIgnoreCase)) continue; var grant = new TrieGrant(row.LdapGroup, row.PermissionFlags); var node = row.ScopeKind switch { NodeAclScopeKind.Cluster => trie.Root, _ => Descend(trie.Root, row, scopePaths), }; if (node is not null) node.Grants.Add(grant); } return trie; } private static PermissionTrieNode? Descend(PermissionTrieNode root, NodeAcl row, IReadOnlyDictionary? scopePaths) { if (string.IsNullOrEmpty(row.ScopeId)) return null; // For sub-cluster scopes the caller supplies a path lookup so we know the containing // namespace / UnsArea / UnsLine ids. Without a path lookup we fall back to putting the // row directly under the root using its ScopeId — works for deterministic tests, not // for production where the hierarchy must be honored. if (scopePaths is null || !scopePaths.TryGetValue(row.ScopeId, out var path)) { return EnsureChild(root, row.ScopeId); } var node = root; foreach (var segment in path.Segments) node = EnsureChild(node, segment); return node; } private static PermissionTrieNode EnsureChild(PermissionTrieNode parent, string key) { if (!parent.Children.TryGetValue(key, out var child)) { child = new PermissionTrieNode(); parent.Children[key] = child; } return child; } } /// /// Ordered list of trie-path segments from root to the target node. Supplied to /// so the builder knows where a /// -scoped row sits in the hierarchy. /// /// /// Namespace id, then (for Equipment kind) UnsAreaId / UnsLineId / EquipmentId / TagId as /// applicable; or (for SystemPlatform kind) NamespaceId / FolderSegment / .../TagId. /// public sealed record NodeAclPath(IReadOnlyList Segments);