using System.Text.RegularExpressions; namespace NATS.Server.Auth.Jwt; /// /// Expands mustache-style template strings in NATS JWT permission subjects. /// When a user connects with a JWT, template strings in their permissions are /// expanded using claim values from the user and account JWTs. /// /// /// Reference: Go auth.go:424-520 — processUserPermissionsTemplate() /// /// Supported template functions: /// {{name()}} — user's Name claim /// {{subject()}} — user's Subject (NKey public key) /// {{tag(tagname)}} — user tags matching "tagname:" prefix (multi-value → cartesian product) /// {{account-name()}} — account display name /// {{account-subject()}} — account NKey public key /// {{account-tag(tagname)}} — account tags matching "tagname:" prefix (multi-value → cartesian product) /// /// When a template resolves to multiple values (e.g. a user with two "dept:" tags), /// the cartesian product of all expanded subjects is returned. If any template /// resolves to zero values, the entire pattern is dropped (returns empty list). /// public static partial class PermissionTemplates { [GeneratedRegex(@"\{\{([^}]+)\}\}")] private static partial Regex TemplateRegex(); /// /// Expands a single permission pattern containing zero or more template expressions. /// Returns the list of concrete subjects after substitution. /// Returns an empty list if any template resolves to no values (tag not found). /// Returns a single-element list containing the original pattern if no templates are present. /// public static List Expand( string pattern, string name, string subject, string accountName, string accountSubject, string[] userTags, string[] accountTags) { var matches = TemplateRegex().Matches(pattern); if (matches.Count == 0) return [pattern]; var replacements = new List<(string Placeholder, string[] Values)>(); foreach (Match match in matches) { var expr = match.Groups[1].Value.Trim(); var values = ResolveTemplate(expr, name, subject, accountName, accountSubject, userTags, accountTags); if (values.Length == 0) return []; replacements.Add((match.Value, values)); } // Compute cartesian product across all multi-value replacements. // Start with the full pattern and iteratively replace each placeholder. var results = new List { pattern }; foreach (var (placeholder, values) in replacements) { var next = new List(); foreach (var current in results) foreach (var value in values) next.Add(current.Replace(placeholder, value)); results = next; } return results; } /// /// Expands all patterns in a permission list, flattening multi-value expansions /// into the result. Patterns that resolve to no values are omitted entirely. /// public static List ExpandAll( IEnumerable patterns, string name, string subject, string accountName, string accountSubject, string[] userTags, string[] accountTags) { var result = new List(); foreach (var pattern in patterns) result.AddRange(Expand(pattern, name, subject, accountName, accountSubject, userTags, accountTags)); return result; } private static string[] ResolveTemplate( string expr, string name, string subject, string accountName, string accountSubject, string[] userTags, string[] accountTags) { return expr.ToLowerInvariant() switch { "name()" => [name], "subject()" => [subject], "account-name()" => [accountName], "account-subject()" => [accountSubject], _ when expr.StartsWith("tag(", StringComparison.OrdinalIgnoreCase) => ResolveTags(expr, userTags), _ when expr.StartsWith("account-tag(", StringComparison.OrdinalIgnoreCase) => ResolveTags(expr, accountTags), _ => [] }; } /// /// Extracts the tag name from a tag() or account-tag() expression and returns /// all matching tag values from the provided tags array. /// Tags are stored in "key:value" format; this method returns the value portion. /// private static string[] ResolveTags(string expr, string[] tags) { var openParen = expr.IndexOf('('); var closeParen = expr.IndexOf(')'); if (openParen < 0 || closeParen < 0) return []; var tagName = expr[(openParen + 1)..closeParen].Trim(); var prefix = tagName + ":"; return tags .Where(t => t.StartsWith(prefix, StringComparison.Ordinal)) .Select(t => t[prefix.Length..]) .ToArray(); } }