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>