124 lines
5.0 KiB
C#
124 lines
5.0 KiB
C#
using System.Text.RegularExpressions;
|
|
|
|
namespace NATS.Server.Auth.Jwt;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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).
|
|
/// </remarks>
|
|
public static partial class PermissionTemplates
|
|
{
|
|
[GeneratedRegex(@"\{\{([^}]+)\}\}")]
|
|
private static partial Regex TemplateRegex();
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static List<string> 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<string> { pattern };
|
|
foreach (var (placeholder, values) in replacements)
|
|
{
|
|
var next = new List<string>();
|
|
foreach (var current in results)
|
|
foreach (var value in values)
|
|
next.Add(current.Replace(placeholder, value));
|
|
results = next;
|
|
}
|
|
return results;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expands all patterns in a permission list, flattening multi-value expansions
|
|
/// into the result. Patterns that resolve to no values are omitted entirely.
|
|
/// </summary>
|
|
public static List<string> ExpandAll(
|
|
IEnumerable<string> patterns,
|
|
string name, string subject,
|
|
string accountName, string accountSubject,
|
|
string[] userTags, string[] accountTags)
|
|
{
|
|
var result = new List<string>();
|
|
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),
|
|
_ => []
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|