-
-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathDefaultResponseParser.cs
More file actions
122 lines (116 loc) · 5.19 KB
/
Copy pathDefaultResponseParser.cs
File metadata and controls
122 lines (116 loc) · 5.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
using Microsoft.AspNetCore.Antiforgery;
using NpgsqlRest;
using NpgsqlRest.Auth;
namespace NpgsqlRestClient;
public class DefaultResponseParser(
NpgsqlRestAuthenticationOptions options,
string? antiforgeryFieldNameTag,
string? antiforgeryTokenTag,
Dictionary<string, string?>? availableClaims,
Dictionary<string, string?>? availableEnvVars = null)
{
// Env vars are resolved ONCE at construction. They don't change within the pod's lifetime, so
// re-reading per request would just add System.Environment overhead with no payoff. A K8s pod
// restart re-instantiates the middleware (and thus this parser), which re-reads the values.
private readonly Dictionary<string, string> _envVarReplacements = ResolveEnvVars(availableEnvVars);
private static Dictionary<string, string> ResolveEnvVars(Dictionary<string, string?>? envVars)
{
if (envVars is null || envVars.Count == 0)
{
return [];
}
var result = new Dictionary<string, string>(envVars.Count);
foreach (var (name, def) in envVars)
{
// present env var wins → configured default → empty string.
var raw = Environment.GetEnvironmentVariable(name) ?? def ?? string.Empty;
// JSON-escape exactly like the claim path below (PgConverters.SerializeString). The
// substituted value is a complete JSON literal (a quoted, escaped string) so templates
// use a bare {NAME} token with no surrounding quotes and an accidental quote/backslash in
// the value cannot break the JS string. NOTE: the relaxed encoder does NOT escape '<'/'>',
// so this is not a defence against a hostile value - env values are operator-controlled,
// matching the claim path's trust model.
// SECURITY: this is an explicit allowlist - only names listed in AvailableEnvVars are ever
// read. NEVER widen this to the whole environment (Environment.GetEnvironmentVariables) -
// that would leak secrets (DB passwords, keys) to every client.
result[name] = PgConverters.SerializeString(raw);
}
return result;
}
public ReadOnlySpan<char> Parse(
ReadOnlySpan<char> input,
HttpContext context,
AntiforgeryTokenSet? tokenSet)
{
Dictionary<string, string> replacements = [];
HashSet<string> arrayTypes = new(10)
{
options.DefaultRoleClaimType
};
foreach (var claim in context.User.Claims)
{
if (replacements.TryGetValue(claim.Type, out var existingValue))
{
replacements[claim.Type] = string.Concat(existingValue, ",", PgConverters.SerializeString(claim.Value));
if (arrayTypes.Contains(claim.Type) is false)
{
arrayTypes.Add(claim.Type);
}
}
else
{
replacements.Add(claim.Type, PgConverters.SerializeString(claim.Value));
}
}
foreach (var item in arrayTypes)
{
if (replacements.TryGetValue(item, out var existingValue) is true)
{
replacements[item] = string.Concat(Consts.OpenBracket, existingValue, Consts.CloseBracket);
}
else
{
replacements[item] = string.Concat(Consts.OpenBracket, Consts.CloseBracket);
}
}
if (replacements.ContainsKey(options.DefaultUserIdClaimType) is false)
{
replacements.Add(options.DefaultUserIdClaimType, Consts.Null);
}
if (replacements.ContainsKey(options.DefaultNameClaimType) is false)
{
replacements.Add(options.DefaultNameClaimType, Consts.Null);
}
if (tokenSet is not null && (antiforgeryFieldNameTag is not null || antiforgeryTokenTag is not null))
{
if (antiforgeryFieldNameTag is not null)
{
replacements.Add(antiforgeryFieldNameTag, tokenSet.FormFieldName);
}
if (antiforgeryTokenTag is not null && tokenSet.RequestToken is not null)
{
replacements.Add(antiforgeryTokenTag, tokenSet.RequestToken);
}
}
// Listed-but-absent claims fall back to their configured default, or Consts.Null (the
// historical behaviour) when the array form gives no explicit default. Claim values added
// above from context.User.Claims are already present and win via TryAdd.
if (availableClaims is not null && availableClaims.Count > 0)
{
foreach (var (name, def) in availableClaims)
{
replacements.TryAdd(name, def ?? Consts.Null);
}
}
// Feed app-wide env-var values into the same dictionary. Per-request claim values added above
// WIN over env vars if the names ever collide (TryAdd does not overwrite).
if (_envVarReplacements.Count > 0)
{
foreach (var (name, value) in _envVarReplacements)
{
replacements.TryAdd(name, value);
}
}
return Formatter.FormatString(input, replacements);
}
}