From 4a23a7624256b3022f0f07e058ef0e7b9ad16c56 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Tue, 29 Oct 2024 17:52:15 +0300 Subject: [PATCH 1/5] Validate openapi document before plugin generation --- .../Plugins/PluginsGenerationService.cs | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index ef638601fa..d9634e8fa4 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -10,10 +10,12 @@ using Kiota.Builder.Extensions; using Kiota.Builder.OpenApiExtensions; using Microsoft.OpenApi.ApiManifest; +using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; using Microsoft.OpenApi.Writers; using Microsoft.Plugins.Manifest; +using Microsoft.Plugins.Manifest.OpenApiRules; namespace Kiota.Builder.Plugins; @@ -42,10 +44,27 @@ public PluginsGenerationService(OpenApiDocument document, OpenApiUrlTreeNode ope private const string DescriptionPathSuffix = "openapi.yml"; public async Task GenerateManifestAsync(CancellationToken cancellationToken = default) { - // 1. cleanup any namings to be used later on. + + //1. validate openapi file + var ruleSet = new Microsoft.OpenApi.Validations.ValidationRuleSet + { + OpenApiServerUrlRule.ServerUrlMustBeHttps, + OpenApiCombinedAuthFlowRule.PathsCanOnlyHaveOneSecuritySchemePerOperation(OAIDocument.SecurityRequirements), + OpenApiRequestBodySchemaRule.RequestBodySchemaObjectsMustNeverBeNested, + OpenApiApiKeyBearerRule.ApiKeyNotSupportedOnlyBearerPlusHttp(OAIDocument.SecurityRequirements) + }; + var errors = OAIDocument.Validate(ruleSet)?.ToArray(); + if (errors != null && errors.Length != 0) + { + var message = string.Join(Environment.NewLine, errors.Select(e => $"{e.Pointer}: {e.Message}")); + throw new InvalidOperationException($"OpenApi document validation failed with errors: {message}"); + } + + // 2. cleanup any namings to be used later on. Configuration.ClientClassName = PluginNameCleanupRegex().Replace(Configuration.ClientClassName, string.Empty); //drop any special characters - // 2. write the OpenApi description + + // 3. write the OpenApi description var descriptionRelativePath = $"{Configuration.ClientClassName.ToLowerInvariant()}-{DescriptionPathSuffix}"; var descriptionFullPath = Path.Combine(Configuration.OutputPath, descriptionRelativePath); var directory = Path.GetDirectoryName(descriptionFullPath); @@ -61,7 +80,7 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de trimmedPluginDocument.SerializeAsV3(descriptionWriter); descriptionWriter.Flush(); - // 3. write the plugins + // 4. write the plugins foreach (var pluginType in Configuration.PluginTypes) { From 747e5773acbe40b0e61fe580b8f9baa77484ea25 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Tue, 29 Oct 2024 17:56:38 +0300 Subject: [PATCH 2/5] Validate openapi before generation --- .../Plugins/PluginsGenerationService.cs | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index d9634e8fa4..f8c82d9c26 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -44,27 +44,11 @@ public PluginsGenerationService(OpenApiDocument document, OpenApiUrlTreeNode ope private const string DescriptionPathSuffix = "openapi.yml"; public async Task GenerateManifestAsync(CancellationToken cancellationToken = default) { - - //1. validate openapi file - var ruleSet = new Microsoft.OpenApi.Validations.ValidationRuleSet - { - OpenApiServerUrlRule.ServerUrlMustBeHttps, - OpenApiCombinedAuthFlowRule.PathsCanOnlyHaveOneSecuritySchemePerOperation(OAIDocument.SecurityRequirements), - OpenApiRequestBodySchemaRule.RequestBodySchemaObjectsMustNeverBeNested, - OpenApiApiKeyBearerRule.ApiKeyNotSupportedOnlyBearerPlusHttp(OAIDocument.SecurityRequirements) - }; - var errors = OAIDocument.Validate(ruleSet)?.ToArray(); - if (errors != null && errors.Length != 0) - { - var message = string.Join(Environment.NewLine, errors.Select(e => $"{e.Pointer}: {e.Message}")); - throw new InvalidOperationException($"OpenApi document validation failed with errors: {message}"); - } - - // 2. cleanup any namings to be used later on. + // 1. cleanup any namings to be used later on. Configuration.ClientClassName = PluginNameCleanupRegex().Replace(Configuration.ClientClassName, string.Empty); //drop any special characters - // 3. write the OpenApi description + // 2. write the OpenApi description var descriptionRelativePath = $"{Configuration.ClientClassName.ToLowerInvariant()}-{DescriptionPathSuffix}"; var descriptionFullPath = Path.Combine(Configuration.OutputPath, descriptionRelativePath); var directory = Path.GetDirectoryName(descriptionFullPath); @@ -80,6 +64,21 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de trimmedPluginDocument.SerializeAsV3(descriptionWriter); descriptionWriter.Flush(); + //3. validate openapi file + var ruleSet = new Microsoft.OpenApi.Validations.ValidationRuleSet + { + OpenApiServerUrlRule.ServerUrlMustBeHttps, + OpenApiCombinedAuthFlowRule.PathsCanOnlyHaveOneSecuritySchemePerOperation(OAIDocument.SecurityRequirements), + OpenApiRequestBodySchemaRule.RequestBodySchemaObjectsMustNeverBeNested, + OpenApiApiKeyBearerRule.ApiKeyNotSupportedOnlyBearerPlusHttp(OAIDocument.SecurityRequirements) + }; + var errors = OAIDocument.Validate(ruleSet)?.ToArray(); + if (errors != null && errors.Length != 0) + { + var message = string.Join(Environment.NewLine, errors.Select(e => $"{e.Pointer}: {e.Message}")); + throw new InvalidOperationException($"OpenApi document validation failed with errors: {message}"); + } + // 4. write the plugins foreach (var pluginType in Configuration.PluginTypes) From e00958a6a8273bee68d37fac92bdf54eb4cf5054 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Wed, 30 Oct 2024 13:06:05 +0300 Subject: [PATCH 3/5] Add unit tests for openapi validation --- .../Plugins/PluginsGenerationService.cs | 3 +- .../Plugins/PluginsGenerationServiceTests.cs | 105 +++++++++++++++++- 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index f8c82d9c26..071640caa4 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -68,9 +68,10 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de var ruleSet = new Microsoft.OpenApi.Validations.ValidationRuleSet { OpenApiServerUrlRule.ServerUrlMustBeHttps, + OpenApiAuthFlowRule.OnlyAuthorizationCodeFlowAllowed(OAIDocument.SecurityRequirements), OpenApiCombinedAuthFlowRule.PathsCanOnlyHaveOneSecuritySchemePerOperation(OAIDocument.SecurityRequirements), OpenApiRequestBodySchemaRule.RequestBodySchemaObjectsMustNeverBeNested, - OpenApiApiKeyBearerRule.ApiKeyNotSupportedOnlyBearerPlusHttp(OAIDocument.SecurityRequirements) + }; var errors = OAIDocument.Validate(ruleSet)?.ToArray(); if (errors != null && errors.Length != 0) diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index 3f74656037..4b18bab066 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -46,7 +46,7 @@ public async Task GeneratesManifestAsync(string inputPluginName, string expected version: 1.0 description: test description we've created servers: - - url: http://localhost/ + - url: https://localhost/ description: There's no place like home paths: /test: @@ -118,6 +118,105 @@ public async Task GeneratesManifestAsync(string inputPluginName, string expected Assert.Empty(resultingManifest.Problems);// no problems are expected with names Assert.Equal("test description we've created", resultingManifest.Document.DescriptionForHuman);// description is pulled from info } + + [Theory] + [InlineData("client", "client")] + [InlineData("Budget Tracker", "BudgetTracker")]//drop the space + [InlineData("My-Super complex() %@#$& Name", "MySupercomplexName")]//drop the space and special characters + public async Task GenerateManifestAsyncFailsOnInvalidOpenApiFile(string inputPluginName, string expectedPluginName) + { + var simpleDescriptionContent = @"openapi: 3.0.0 +info: + title: test + version: 1.0 + description: test description we've created +servers: + - url: http://localhost/ + description: There's no place like home +paths: + /test: + get: + summary: summary for test path + description: description for test path + security: + - OAuth2: [read] + OpenID: [] + responses: + '200': + description: test + /test/{id}: + get: + summary: Summary for test path with id that is longer than 50 characters + description: description for test path with id + operationId: test.WithId + security: + - BasicAuth: [] + parameters: + - name: id + in: path + required: true + description: The id of the test + schema: + type: integer + format: int32 + responses: + '200': + description: test +components: + securitySchemes: + BasicAuth: + type: http + scheme: basic + + BearerAuth: + type: http + scheme: bearer + + ApiKeyAuth: + type: apiKey + in: header + name: X-API-Key + + OpenID: + type: openIdConnect + openIdConnectUrl: https://example.com/.well-known/openid-configuration + + OAuth2: + type: oauth2 + flows: + implicit: + authorizationUrl: https://example.com/api/oauth/dialog + scopes: + write: modify pets in your account + read: read your pets"; + var workingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var simpleDescriptionPath = Path.Combine(workingDirectory) + "description.yaml"; + await File.WriteAllTextAsync(simpleDescriptionPath, simpleDescriptionContent); + 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, PluginType.APIManifest, PluginType.OpenAI], + ClientClassName = inputPluginName, + 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); + KiotaBuilder.CleanupOperationIdForPlugins(openApiDocument); + var urlTreeNode = OpenApiUrlTreeNode.Create(openApiDocument, Constants.DefaultOpenApiLabel); + + var pluginsGenerationService = new PluginsGenerationService(openApiDocument, urlTreeNode, generationConfiguration, workingDirectory); + + var exception = await Assert.ThrowsAsync(async () => await pluginsGenerationService.GenerateManifestAsync()); + Assert.Contains("OpenApi document validation failed", exception.Message); + Assert.Contains("Server URL must use HTTPS protocol", exception.Message); + Assert.Contains("Only Authorization Code flow is allowed for OAuth2", exception.Message); + Assert.Contains("Operation cannot have more than one security scheme", exception.Message); + } + private const string ManifestFileName = "client-apiplugin.json"; private const string OpenAIPluginFileName = "openai-plugins.json"; private const string OpenApiFileName = "client-openapi.yml"; @@ -130,7 +229,7 @@ public async Task ThrowsOnEmptyPathsAfterFilteringAsync() title: test version: 1.0 servers: - - url: http://localhost/ + - url: https://localhost/ description: There's no place like home paths: /test: @@ -165,7 +264,7 @@ public async Task GeneratesManifestAndCleansUpInputDescriptionAsync() version: 1.0 x-test-root-extension: test servers: - - url: http://localhost/ + - url: https://localhost/ description: There's no place like home paths: /test: From da6c37a8c5bd76da0e9a3937f8eecf92550e3067 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Wed, 30 Oct 2024 13:18:38 +0300 Subject: [PATCH 4/5] Remove unused parameters --- .../Plugins/PluginsGenerationServiceTests.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index 4b18bab066..28fa225efd 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -120,10 +120,8 @@ public async Task GeneratesManifestAsync(string inputPluginName, string expected } [Theory] - [InlineData("client", "client")] - [InlineData("Budget Tracker", "BudgetTracker")]//drop the space - [InlineData("My-Super complex() %@#$& Name", "MySupercomplexName")]//drop the space and special characters - public async Task GenerateManifestAsyncFailsOnInvalidOpenApiFile(string inputPluginName, string expectedPluginName) + [InlineData("client")] + public async Task GenerateManifestAsyncFailsOnInvalidOpenApiFile(string inputPluginName) { var simpleDescriptionContent = @"openapi: 3.0.0 info: From 703eb08e733428b21da36f5e1afed8ad06059029 Mon Sep 17 00:00:00 2001 From: "Samwel K." <40166690+samwelkanda@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:39:46 +0300 Subject: [PATCH 5/5] Update src/Kiota.Builder/Plugins/PluginsGenerationService.cs Co-authored-by: Andrew Omondi --- src/Kiota.Builder/Plugins/PluginsGenerationService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 071640caa4..f356ef86b0 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -76,7 +76,7 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de var errors = OAIDocument.Validate(ruleSet)?.ToArray(); if (errors != null && errors.Length != 0) { - var message = string.Join(Environment.NewLine, errors.Select(e => $"{e.Pointer}: {e.Message}")); + var message = string.Join(Environment.NewLine, errors.Select(static e => $"{e.Pointer}: {e.Message}")); throw new InvalidOperationException($"OpenApi document validation failed with errors: {message}"); }