#nullable enable using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using BTCPayServer.Client; namespace BTCPayServer.Services; public record PermissionDefinitionNode( PolicyDefinition Definition, IReadOnlyCollection Children, IReadOnlyCollection Parents) { public IEnumerable EnumerateDescendants(bool includeSelf = true) { if (includeSelf) yield return this; foreach (var descendant in Children.SelectMany(c => c.EnumerateDescendants())) yield return descendant; } public IEnumerable EnumerateParents(bool includeSelf = true) { if (includeSelf) yield return this; foreach (var parent in Parents.SelectMany(p => p.EnumerateParents())) yield return parent; } } public class PermissionService { record PermissionDefinitionNodeBuilder( PolicyDefinition Definition, List Children, List Parents) { public void AddChild(PermissionDefinitionNodeBuilder node) { Children.Add(node); node.Parents.Add(this); } public void Build(Dictionary nodes) { HashSet visited = new(); var unrestricted = Build(nodes, null, visited); AddParents(unrestricted); } private void AddParents(PermissionDefinitionNode node) { foreach (var child in node.Children) { ((List)child.Parents).Add(node); AddParents(child); } } PermissionDefinitionNode Build(Dictionary nodes, PermissionDefinitionNode? parent, HashSet visited) { if (!visited.Add($"{parent} -> {Definition.Policy}")) throw new InvalidOperationException($"Circular reference detected in permissions [{Definition}]"); if (nodes.TryGetValue(Definition.Policy, out var n)) return n; var children = new List(); n = new PermissionDefinitionNode(Definition, children, new List()); nodes.Add(Definition.Policy, n); children.AddRange(Children.Select(c => c.Build(nodes, n, visited))); return n; } } private readonly IReadOnlyDictionary _definitions; public PermissionService(IEnumerable definitions) { var definitionsByPermission = new Dictionary(StringComparer.OrdinalIgnoreCase); var nodes = new Dictionary(); foreach (var definition in definitions) { definitionsByPermission[definition.Policy] = definition; nodes.Add(definition, new PermissionDefinitionNodeBuilder(definition, new(), new())); } _definitions = new ReadOnlyDictionary(definitionsByPermission); foreach (var node in nodes) { foreach (var included in node.Key.IncludedPermissions) node.Value.AddChild(nodes[GetPolicyDefinition(definitionsByPermission, included)]); foreach (var includedBy in node.Key.IncludedByPermissions) nodes[GetPolicyDefinition(definitionsByPermission, includedBy)].AddChild(node.Value); } var unrestricted = nodes[definitionsByPermission[Policies.Unrestricted]]; foreach (var node in nodes) { if (node.Key.Policy == Policies.Unrestricted) continue; if (node.Value.Parents.Count is 0) unrestricted.AddChild(node.Value); } var permNodes = new Dictionary(); unrestricted.Build(permNodes); PermissionNodesByPolicy = new ReadOnlyDictionary(permNodes); UnrestrictedPermissionNode = permNodes[Policies.Unrestricted]; Definitions = _definitions; } private static PolicyDefinition GetPolicyDefinition(Dictionary definitionsByPermission, string included) { if (definitionsByPermission.TryGetValue(included, out var definition)) return definition; throw new ArgumentException($"Permission '{included}' is not defined"); } public IReadOnlyDictionary PermissionNodesByPolicy { get; } public PermissionDefinitionNode UnrestrictedPermissionNode { get; } public IReadOnlyDictionary Definitions { get; } public bool TryGetDefinition(string permission, [MaybeNullWhen(false)] out PolicyDefinition definition) { definition = null; if (string.IsNullOrWhiteSpace(permission)) return false; return _definitions.TryGetValue(permission, out definition); } public PolicyDefinition? TryGetDefinition(string permission) { this.TryGetDefinition(permission, out var definition); return definition; } public bool IsValidPolicy(string policy) { if (string.IsNullOrWhiteSpace(policy)) return false; return _definitions.ContainsKey(policy); } public bool Contains(Permission permission, Permission requestedPermission, bool anyScope = false) { if (permission is null) throw new ArgumentNullException(nameof(permission)); if (requestedPermission is null) throw new ArgumentNullException(nameof(requestedPermission)); if (!ContainsPolicy(permission.Policy, requestedPermission.Policy)) return false; return permission.Scope == null || anyScope || requestedPermission.Scope == permission.Scope; } private bool ContainsPolicy(string policy, string subpolicy) { if (!PermissionNodesByPolicy.TryGetValue(policy, out var policyNode) || !PermissionNodesByPolicy.TryGetValue(subpolicy, out var subPolicyNode)) return false; return subPolicyNode.EnumerateParents().Any(p => p == policyNode); } }