perf(auth): allocation-free token parsing; single partition-key build per RPC
This commit is contained in:
+39
-16
@@ -77,8 +77,13 @@ public sealed class GatewayGrpcAuthorizationInterceptor(
|
||||
// aggregate for the key id. An over-limit state still admits one probe per interval, so the
|
||||
// holder of the correct secret always reaches the verifier and resets the state.
|
||||
// ResourceExhausted signals throttling without revealing whether any secret was valid.
|
||||
//
|
||||
// The check hands back the storage key it resolved so the reset below reuses it instead of
|
||||
// rebuilding the composite (peer, key id) string a second time on every successful RPC.
|
||||
ApiKeyThrottlePartition throttlePartition = ResolveThrottlePartition(authorizationHeader, context);
|
||||
ApiKeyThrottleDecision decision = failureLimiter.Check(throttlePartition);
|
||||
ApiKeyThrottleDecision decision = failureLimiter.Check(
|
||||
throttlePartition,
|
||||
out ApiKeyFailureLimiter.PartitionResolution throttleResolution);
|
||||
if (decision is ApiKeyThrottleDecision.ThrottledByPeer or ApiKeyThrottleDecision.ThrottledByAggregate)
|
||||
{
|
||||
metrics.RecordAuthThrottled(
|
||||
@@ -107,7 +112,7 @@ public sealed class GatewayGrpcAuthorizationInterceptor(
|
||||
// fat-fingered a few attempts is not penalised once it recovers — and, because the check
|
||||
// above admits a probe rather than blocking absolutely, this reset stays reachable while the
|
||||
// key is under an active spray.
|
||||
failureLimiter.Reset(throttlePartition);
|
||||
failureLimiter.Reset(throttlePartition, throttleResolution);
|
||||
|
||||
ApiKeyIdentity identity = GatewayApiKeyIdentityMapper.ToGatewayIdentity(verification.Identity);
|
||||
|
||||
@@ -137,7 +142,11 @@ public sealed class GatewayGrpcAuthorizationInterceptor(
|
||||
// before a key-id partition is minted so a spray of invented tokens cannot mint one tracked
|
||||
// partition each and flush the limiter's bounded map (SEC-32). Anything that fails the check
|
||||
// falls back to the sender's transport-peer partition.
|
||||
private static string? TryResolveKeyId(string? authorizationHeader)
|
||||
//
|
||||
// Internal rather than private so the parse rules can be pinned directly by test; the shape
|
||||
// check is a security boundary (SEC-32) and is worth asserting without routing every case
|
||||
// through a full RPC.
|
||||
internal static string? TryResolveKeyId(string? authorizationHeader)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(authorizationHeader))
|
||||
{
|
||||
@@ -150,22 +159,36 @@ public sealed class GatewayGrpcAuthorizationInterceptor(
|
||||
? header[bearer.Length..].Trim()
|
||||
: header;
|
||||
|
||||
string[] parts = token.ToString().Split('_');
|
||||
if (parts.Length < 3)
|
||||
// Scanned rather than split: this runs on every authenticated RPC, and Split would copy the
|
||||
// token out of the header and allocate an array plus a string per segment to reach a key id
|
||||
// that is then usually a dictionary-lookup miss. Two IndexOf scans reach the same answer and
|
||||
// allocate only the key id itself.
|
||||
const string prefix = AuthStoreServiceCollectionExtensions.TokenPrefix;
|
||||
if (!token.StartsWith(prefix, StringComparison.Ordinal)
|
||||
|| token.Length <= prefix.Length
|
||||
|| token[prefix.Length] != '_')
|
||||
{
|
||||
// Guards the whole first segment, not just its start: the '_' immediately after the
|
||||
// prefix is what makes "mxgw" the entire segment, so "mxgwabc_..." is still rejected.
|
||||
return null;
|
||||
}
|
||||
|
||||
ReadOnlySpan<char> afterPrefix = token[(prefix.Length + 1)..];
|
||||
int separator = afterPrefix.IndexOf('_');
|
||||
if (separator <= 0 || separator > MaxKeyIdLength)
|
||||
{
|
||||
// -1 is a token with no second separator (too few segments); 0 is an empty key id.
|
||||
return null;
|
||||
}
|
||||
|
||||
// The third segment must be non-empty, which the split form expressed as parts[2].Length: it
|
||||
// ends at the NEXT separator, so a secret beginning with '_' fails the same way it always did.
|
||||
ReadOnlySpan<char> afterKeyId = afterPrefix[(separator + 1)..];
|
||||
if (afterKeyId.IsEmpty || afterKeyId[0] == '_')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!string.Equals(parts[0], AuthStoreServiceCollectionExtensions.TokenPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parts[1].Length == 0 || parts[1].Length > MaxKeyIdLength || parts[2].Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return parts[1];
|
||||
return new string(afterPrefix[..separator]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user