Skip to content

Commit 4252a4a

Browse files
committed
fix: make security scheme overrides authoritative
1 parent ef12363 commit 4252a4a

File tree

3 files changed

+228
-1
lines changed

3 files changed

+228
-1
lines changed

src/libs/AutoSDK/Extensions/OpenApiExtensions.cs

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,39 @@ namespace AutoSDK.Extensions;
1414

1515
public static class OpenApiExtensions
1616
{
17+
private readonly struct SecurityParameterMatcher : IEquatable<SecurityParameterMatcher>
18+
{
19+
public SecurityParameterMatcher(
20+
ParameterLocation location,
21+
string name)
22+
{
23+
Location = location;
24+
Name = name;
25+
}
26+
27+
public ParameterLocation Location { get; }
28+
public string Name { get; }
29+
30+
public bool Equals(SecurityParameterMatcher other)
31+
{
32+
return Location == other.Location &&
33+
string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase);
34+
}
35+
36+
public override bool Equals(object? obj)
37+
{
38+
return obj is SecurityParameterMatcher other && Equals(other);
39+
}
40+
41+
public override int GetHashCode()
42+
{
43+
unchecked
44+
{
45+
return ((int)Location * 397) ^ StringComparer.OrdinalIgnoreCase.GetHashCode(Name ?? string.Empty);
46+
}
47+
}
48+
}
49+
1750
public static OpenApiDocument GetOpenApiDocument(
1851
this string yamlOrJson,
1952
Settings settings,
@@ -1094,7 +1127,9 @@ public static void InjectSecuritySchemes(
10941127
{
10951128
openApiDocument = openApiDocument ?? throw new ArgumentNullException(nameof(openApiDocument));
10961129

1097-
openApiDocument.Components!.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
1130+
openApiDocument.Components!.SecuritySchemes = new Dictionary<string, IOpenApiSecurityScheme>();
1131+
openApiDocument.Security = new List<OpenApiSecurityRequirement>();
1132+
var matchers = new HashSet<SecurityParameterMatcher>();
10981133

10991134
foreach (var scheme in settings.SecuritySchemes)
11001135
{
@@ -1146,6 +1181,80 @@ public static void InjectSecuritySchemes(
11461181
{
11471182
[schemeRef] = new List<string>(),
11481183
});
1184+
if (TryCreateSecurityParameterMatcher(schemeType, location, namePart, out var matcher))
1185+
{
1186+
matchers.Add(matcher);
1187+
}
1188+
}
1189+
1190+
var pathItems = openApiDocument.Paths != null
1191+
? openApiDocument.Paths.Values.ToList()
1192+
: new List<IOpenApiPathItem>();
1193+
foreach (var pathItem in pathItems)
1194+
{
1195+
SuppressMatchingSecurityParameters(pathItem.Parameters, matchers);
1196+
1197+
foreach (var operation in pathItem.Operations?.Values ?? Enumerable.Empty<OpenApiOperation>())
1198+
{
1199+
operation.Security = new List<OpenApiSecurityRequirement>();
1200+
SuppressMatchingSecurityParameters(operation.Parameters, matchers);
1201+
}
1202+
}
1203+
}
1204+
1205+
private static void SuppressMatchingSecurityParameters(
1206+
IList<IOpenApiParameter>? parameters,
1207+
ISet<SecurityParameterMatcher> matchers)
1208+
{
1209+
if (parameters == null || parameters.Count == 0 || matchers.Count == 0)
1210+
{
1211+
return;
1212+
}
1213+
1214+
for (var i = parameters.Count - 1; i >= 0; i--)
1215+
{
1216+
var parameter = parameters[i];
1217+
if (string.IsNullOrWhiteSpace(parameter.Name))
1218+
{
1219+
continue;
1220+
}
1221+
1222+
if (parameter.In is not ParameterLocation parameterLocation)
1223+
{
1224+
continue;
1225+
}
1226+
1227+
var matcher = new SecurityParameterMatcher(parameterLocation, parameter.Name!);
1228+
if (matchers.Contains(matcher) ||
1229+
matchers.Any(x =>
1230+
x.Location == parameterLocation &&
1231+
string.Equals(x.Name, parameter.Name, StringComparison.OrdinalIgnoreCase)))
1232+
{
1233+
parameters.RemoveAt(i);
1234+
}
1235+
}
1236+
}
1237+
1238+
private static bool TryCreateSecurityParameterMatcher(
1239+
SecuritySchemeType schemeType,
1240+
ParameterLocation location,
1241+
string name,
1242+
out SecurityParameterMatcher matcher)
1243+
{
1244+
matcher = default;
1245+
1246+
switch (schemeType)
1247+
{
1248+
case SecuritySchemeType.Http:
1249+
case SecuritySchemeType.OAuth2:
1250+
case SecuritySchemeType.OpenIdConnect:
1251+
matcher = new SecurityParameterMatcher(ParameterLocation.Header, "Authorization");
1252+
return true;
1253+
case SecuritySchemeType.ApiKey when !string.IsNullOrWhiteSpace(name):
1254+
matcher = new SecurityParameterMatcher(location, name);
1255+
return true;
1256+
default:
1257+
return false;
11491258
}
11501259
}
11511260

src/tests/AutoSDK.IntegrationTests.Cli/CliTests.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,69 @@ await GenerateFromContentAsync(
573573
additionalArguments: ["--security-scheme", "Http:Header:Token"]);
574574
}
575575

576+
[TestMethod]
577+
public async Task Generate_WithSecuritySchemeOverride_ReplacesAuthAndSuppressesDuplicateParameters()
578+
{
579+
const string spec = """
580+
openapi: 3.0.3
581+
info:
582+
title: Auth Override
583+
version: 1.0.0
584+
components:
585+
securitySchemes:
586+
queryKey:
587+
type: apiKey
588+
in: query
589+
name: api_key
590+
paths:
591+
/chat:
592+
get:
593+
operationId: getChat
594+
security:
595+
- queryKey: []
596+
parameters:
597+
- in: header
598+
name: Authorization
599+
schema:
600+
type: string
601+
- in: query
602+
name: keep
603+
schema:
604+
type: string
605+
responses:
606+
'200':
607+
description: OK
608+
content:
609+
application/json:
610+
schema:
611+
type: object
612+
properties:
613+
ok:
614+
type: boolean
615+
""";
616+
617+
await GenerateFromContentAsync(
618+
fileName: "security-override.yaml",
619+
specContent: spec,
620+
targetFramework: "net10.0",
621+
namespaceValue: "AuthOverride",
622+
clientClassName: "AuthOverrideClient",
623+
assertGeneratedOutput: async outputDirectory =>
624+
{
625+
var generatedContents = await Task.WhenAll(
626+
Directory.EnumerateFiles(outputDirectory, "*.g.cs", SearchOption.AllDirectories)
627+
.Select(path => File.ReadAllTextAsync(path)));
628+
var content = string.Join("\n\n", generatedContents);
629+
630+
content.Should().Contain("AuthenticationHeaderValue(");
631+
content.Should().Contain("\"Bearer\"");
632+
content.Should().NotContain("ApiKeyInQuery");
633+
content.Should().NotContain("string? authorization = default");
634+
content.Should().Contain("string? keep = default");
635+
},
636+
additionalArguments: ["--security-scheme", "Http:Header:Bearer"]);
637+
}
638+
576639
[TestMethod]
577640
public async Task Generate_WithDuplicateQueryParameterNames_Builds()
578641
{

src/tests/AutoSDK.UnitTests/Tests.InjectSecuritySchemes.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,4 +249,59 @@ public void InjectSecuritySchemes_InvalidFormat_Skips()
249249
// Assert
250250
document.Security.Should().BeEmpty();
251251
}
252+
253+
[TestMethod]
254+
public void InjectSecuritySchemes_ReplacesOperationSecurity_AndSuppressesMatchingParameters()
255+
{
256+
var yaml = """
257+
openapi: 3.0.3
258+
info:
259+
title: Auth Operation Security
260+
version: 1.0.0
261+
components:
262+
securitySchemes:
263+
queryKey:
264+
type: apiKey
265+
in: query
266+
name: api_key
267+
paths:
268+
/chat:
269+
get:
270+
operationId: getChat
271+
security:
272+
- queryKey: []
273+
parameters:
274+
- in: header
275+
name: Authorization
276+
schema:
277+
type: string
278+
- in: query
279+
name: keep
280+
schema:
281+
type: string
282+
responses:
283+
'200':
284+
description: ok
285+
""";
286+
287+
var settings = Settings.Default with
288+
{
289+
SecuritySchemes = new[] { "Http:Header:Bearer" }.ToImmutableArray(),
290+
};
291+
292+
var document = yaml.GetOpenApiDocument(settings);
293+
var pathItem = document.Paths["/chat"];
294+
pathItem.Should().NotBeNull();
295+
pathItem!.Operations.Should().NotBeNull();
296+
var operation = pathItem.Operations![System.Net.Http.HttpMethod.Get];
297+
298+
document.Components!.SecuritySchemes.Should().ContainSingle();
299+
document.Security!.Should().ContainSingle();
300+
document.Security[0].Keys.Single().Scheme.Should().Be("Bearer");
301+
302+
operation.Security.Should().NotBeNull();
303+
operation.Security!.Should().BeEmpty();
304+
operation.Parameters.Should().ContainSingle();
305+
operation.Parameters![0].Name.Should().Be("keep");
306+
}
252307
}

0 commit comments

Comments
 (0)