diff --git a/src/Kiota.Builder/Plugins/AuthComparer.cs b/src/Kiota.Builder/Plugins/AuthComparer.cs
index 52f5c55c0a..cf71b0c97f 100644
--- a/src/Kiota.Builder/Plugins/AuthComparer.cs
+++ b/src/Kiota.Builder/Plugins/AuthComparer.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Linq;
using Microsoft.Plugins.Manifest;
namespace Kiota.Builder.Plugins;
@@ -12,8 +13,15 @@ internal class AuthComparer(StringComparer? stringComparer = null) : IEqualityCo
public bool Equals(Auth? x, Auth? y)
{
if (x is null || y is null) return object.Equals(x, y);
- // TODO: Should we compare the reference id as well?
- return _stringComparer.Equals(x.Type, y.Type);
+ return x switch
+ {
+ AnonymousAuth when y is AnonymousAuth => true,
+ ApiKeyPluginVault x0 when y is ApiKeyPluginVault y0 => _stringComparer.Equals(x0.ReferenceId,
+ y0.ReferenceId),
+ OAuthPluginVault x1 when y is OAuthPluginVault y1 => _stringComparer.Equals(x1.ReferenceId, y1.ReferenceId),
+ EntraOnBehalfOf x2 when y is EntraOnBehalfOf y2 => (x2.Scopes ?? []).SequenceEqual(y2.Scopes ?? []),
+ _ => false
+ };
}
///
public int GetHashCode([DisallowNull] Auth obj)
@@ -21,6 +29,22 @@ public int GetHashCode([DisallowNull] Auth obj)
var hash = new HashCode();
if (obj == null) return hash.ToHashCode();
hash.Add(obj.Type, _stringComparer);
+ switch (obj)
+ {
+ case ApiKeyPluginVault o0:
+ hash.Add(o0.ReferenceId, _stringComparer);
+ break;
+ case OAuthPluginVault o1:
+ hash.Add(o1.ReferenceId, _stringComparer);
+ break;
+ case EntraOnBehalfOf o2:
+ foreach (var scope in o2.Scopes ?? [])
+ {
+ hash.Add(scope, _stringComparer);
+ }
+ break;
+ }
+
return hash.ToHashCode();
}
}
diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs
index 4ec587549b..ef638601fa 100644
--- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs
+++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs
@@ -352,7 +352,7 @@ private static (OpenApiRuntime[], Function[], ConversationStarter[]) GetRuntimes
{
// Configuration overrides document information
Auth = configAuth ?? GetAuth(operation.Security ?? document.SecurityRequirements),
- Spec = new OpenApiRuntimeSpec { Url = openApiDocumentPath, },
+ Spec = new OpenApiRuntimeSpec { Url = openApiDocumentPath },
RunForFunctions = [operation.OperationId]
});
@@ -387,9 +387,14 @@ private static (OpenApiRuntime[], Function[], ConversationStarter[]) GetRuntimes
private static Auth GetAuth(IList securityRequirements)
{
- // Only one security object is allowed
- var security = securityRequirements.SingleOrDefault();
- var opSecurity = security?.Keys.SingleOrDefault();
+ // Only one security requirement object is allowed
+ const string tooManySchemesError = "Multiple security requirements are not supported. Operations can only list one security requirement.";
+ if (securityRequirements.Count > 1 || securityRequirements.FirstOrDefault()?.Keys.Count > 1)
+ {
+ throw new InvalidOperationException(tooManySchemesError);
+ }
+ var security = securityRequirements.FirstOrDefault();
+ var opSecurity = security?.Keys.FirstOrDefault();
return (opSecurity is null || opSecurity.UnresolvedReference) ? new AnonymousAuth() : GetAuthFromSecurityScheme(opSecurity);
}
diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs
index c55fdb3ade..3f74656037 100644
--- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs
+++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs
@@ -276,12 +276,14 @@ public async Task GeneratesManifestAndCleansUpInputDescriptionAsync()
Assert.Single(resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Extensions); // 1 supported extension still present in operation
}
+ #region Security
+
public static TheoryData>>
SecurityInformationSuccess()
{
return new TheoryData>>
{
- // security scheme in operation object
+ // security requirement in operation object
{
"{securitySchemes: {apiKey0: {type: apiKey, name: x-api-key, in: header }}}",
string.Empty, "security: [apiKey0: []]", null, resultingManifest =>
@@ -295,7 +297,21 @@ public static TheoryData
+ {
+ Assert.NotNull(resultingManifest.Document);
+ Assert.Empty(resultingManifest.Problems);
+ Assert.NotEmpty(resultingManifest.Document.Runtimes);
+ var auth0 = resultingManifest.Document.Runtimes[0].Auth;
+ Assert.IsType(auth0);
+ Assert.Equal(AuthType.ApiKeyPluginVault, auth0?.Type);
+ Assert.Equal("{apiKey0_REGISTRATION_ID}", ((ApiKeyPluginVault)auth0!).ReferenceId);
+ }
+ },
+ // security requirement in root object
// TODO: Revisit when https://github.com/microsoft/OpenAPI.NET/issues/1797 is fixed
// {
// "{securitySchemes: {apiKey0: {type: apiKey, name: x-api-key, in: header }}}",
@@ -556,6 +572,96 @@ await assertions(async () =>
}
}
+ [Fact]
+ public async Task GeneratesManifestWithMultipleSecuritySchemesAsync()
+ {
+ var apiDescription = """
+ openapi: 3.0.0
+ info:
+ title: test
+ version: "1.0"
+ servers:
+ - url: https://localhost:8080
+ paths:
+ /test:
+ get:
+ description: description for test path
+ responses:
+ "200":
+ description: test
+ security: [{apiKey0: []}]
+ patch:
+ description: description for test path
+ responses:
+ "200":
+ description: test
+ security: [{apiKey1: []}]
+ components:
+ {
+ securitySchemes: {
+ apiKey0: { type: apiKey, name: x-api-key0, in: header },
+ apiKey1: { type: apiKey, name: x-api-key1, in: header },
+ },
+ }
+ """;
+ var workingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ var simpleDescriptionPath = Path.Combine(workingDirectory) + "description.yaml";
+ await File.WriteAllTextAsync(simpleDescriptionPath, apiDescription);
+ var mockLogger = new Mock>();
+ var openApiDocumentDs = new OpenApiDocumentDownloadService(_httpClient, mockLogger.Object);
+ var outputDirectory = Path.Combine(workingDirectory, "output");
+ var generationConfiguration = new GenerationConfiguration
+ {
+ OutputPath = outputDirectory,
+ OpenAPIFilePath = "openapiPath",
+ PluginTypes = [PluginType.APIPlugin],
+ ClientClassName = "client",
+ ApiRootUrl = "http://localhost/", //Kiota builder would set this for us
+ };
+ var (openApiDocumentStream, _) =
+ await openApiDocumentDs.LoadStreamAsync(simpleDescriptionPath, generationConfiguration, null, false);
+ var openApiDocument =
+ await openApiDocumentDs.GetDocumentFromStreamAsync(openApiDocumentStream, generationConfiguration);
+ Assert.NotNull(openApiDocument);
+ KiotaBuilder.CleanupOperationIdForPlugins(openApiDocument);
+ var urlTreeNode = OpenApiUrlTreeNode.Create(openApiDocument, Constants.DefaultOpenApiLabel);
+
+ var pluginsGenerationService =
+ new PluginsGenerationService(openApiDocument, urlTreeNode, generationConfiguration, workingDirectory);
+ await pluginsGenerationService.GenerateManifestAsync();
+
+ Assert.True(File.Exists(Path.Combine(outputDirectory, ManifestFileName)));
+ Assert.True(File.Exists(Path.Combine(outputDirectory, OpenApiFileName)));
+
+ // Validate the v2 plugin
+ var manifestContent = await File.ReadAllTextAsync(Path.Combine(outputDirectory, ManifestFileName));
+ using var jsonDocument = JsonDocument.Parse(manifestContent);
+ var resultingManifest = PluginManifestDocument.Load(jsonDocument.RootElement);
+
+ Assert.NotNull(resultingManifest.Document);
+ Assert.Empty(resultingManifest.Problems);
+ Assert.NotEmpty(resultingManifest.Document.Runtimes);
+ var auth0 = resultingManifest.Document.Runtimes[0].Auth;
+ Assert.IsType(auth0);
+ Assert.Equal(AuthType.ApiKeyPluginVault, auth0.Type);
+ Assert.Equal("{apiKey0_REGISTRATION_ID}", ((ApiKeyPluginVault)auth0!).ReferenceId);
+ var auth1 = resultingManifest.Document.Runtimes[1].Auth;
+ Assert.IsType(auth1);
+ Assert.Equal(AuthType.ApiKeyPluginVault, auth1.Type);
+ Assert.Equal("{apiKey1_REGISTRATION_ID}", ((ApiKeyPluginVault)auth1!).ReferenceId);
+ // Cleanup
+ try
+ {
+ Directory.Delete(outputDirectory);
+ }
+ catch (Exception)
+ {
+ // ignored
+ }
+ }
+
+ #endregion
+
#region Validation
public static TheoryData>