-
Notifications
You must be signed in to change notification settings - Fork 218
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add auth into plugin generation (#5209)
* add auth into plugin generation * add http bearer security allow falling back to the global security if the operation has none. * fix compile error * refactor exception to its own file * fix security scheme reference id generation * update tests to check for the root security object. * allow configuring plugin manifest's auth * apply formatter * add changelog entry * refactor local function add tests * fix lint error * fix typo in test data * update code docs * fix issue with incorrect auth type when security schemes component does not contain the provided authentication * add auth information to workspace management * fix format * fix test failure * update hashcode * improve hash calculation
- Loading branch information
1 parent
66b5a23
commit 76fa86f
Showing
10 changed files
with
542 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 47 additions & 0 deletions
47
src/Kiota.Builder/Configuration/PluginAuthConfiguration.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
using System; | ||
using Microsoft.Plugins.Manifest; | ||
|
||
namespace Kiota.Builder.Configuration; | ||
|
||
/// <summary> | ||
/// Auth information used in generated plugin manifest | ||
/// </summary> | ||
public class PluginAuthConfiguration | ||
{ | ||
/// <summary> | ||
/// Auth information used in generated plugin manifest | ||
/// </summary> | ||
/// <param name="referenceId">The auth reference id</param> | ||
/// <exception cref="ArgumentException">If the reference id is null or contains only whitespaces.</exception> | ||
public PluginAuthConfiguration(string referenceId) | ||
{ | ||
ArgumentException.ThrowIfNullOrWhiteSpace(referenceId); | ||
ReferenceId = referenceId; | ||
} | ||
|
||
/// <summary> | ||
/// The Teams Toolkit compatible plugin auth type. | ||
/// </summary> | ||
public PluginAuthType AuthType | ||
{ | ||
get; set; | ||
} | ||
|
||
/// <summary> | ||
/// The Teams Toolkit plugin auth reference id | ||
/// </summary> | ||
public string ReferenceId | ||
{ | ||
get; set; | ||
} | ||
|
||
internal Auth ToPluginManifestAuth() | ||
{ | ||
return AuthType switch | ||
{ | ||
PluginAuthType.OAuthPluginVault => new OAuthPluginVault { ReferenceId = ReferenceId }, | ||
PluginAuthType.ApiKeyPluginVault => new ApiKeyPluginVault { ReferenceId = ReferenceId }, | ||
_ => throw new ArgumentOutOfRangeException(nameof(AuthType), $"Unknown plugin auth type '{AuthType}'") | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
namespace Kiota.Builder.Configuration; | ||
|
||
/// <summary> | ||
/// Supported plugin types | ||
/// </summary> | ||
public enum PluginAuthType | ||
{ | ||
/// <summary> | ||
/// OAuth authentication | ||
/// </summary> | ||
OAuthPluginVault, | ||
/// <summary> | ||
/// API key, HTTP Bearer token or OpenId Connect authentication | ||
/// </summary> | ||
ApiKeyPluginVault | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,20 +12,23 @@ | |
using Kiota.Builder.OpenApiExtensions; | ||
using Microsoft.Kiota.Abstractions.Extensions; | ||
using Microsoft.OpenApi.ApiManifest; | ||
using Microsoft.OpenApi.Extensions; | ||
using Microsoft.OpenApi.Models; | ||
using Microsoft.OpenApi.Services; | ||
using Microsoft.OpenApi.Writers; | ||
using Microsoft.Plugins.Manifest; | ||
|
||
namespace Kiota.Builder.Plugins; | ||
|
||
public partial class PluginsGenerationService | ||
{ | ||
private readonly OpenApiDocument OAIDocument; | ||
private readonly OpenApiUrlTreeNode TreeNode; | ||
private readonly GenerationConfiguration Configuration; | ||
private readonly string WorkingDirectory; | ||
|
||
public PluginsGenerationService(OpenApiDocument document, OpenApiUrlTreeNode openApiUrlTreeNode, GenerationConfiguration configuration, string workingDirectory) | ||
public PluginsGenerationService(OpenApiDocument document, OpenApiUrlTreeNode openApiUrlTreeNode, | ||
GenerationConfiguration configuration, string workingDirectory) | ||
{ | ||
ArgumentNullException.ThrowIfNull(document); | ||
ArgumentNullException.ThrowIfNull(openApiUrlTreeNode); | ||
|
@@ -36,13 +39,15 @@ public PluginsGenerationService(OpenApiDocument document, OpenApiUrlTreeNode ope | |
Configuration = configuration; | ||
WorkingDirectory = workingDirectory; | ||
} | ||
|
||
private static readonly OpenAPIRuntimeComparer _openAPIRuntimeComparer = new(); | ||
private const string ManifestFileNameSuffix = ".json"; | ||
private const string DescriptionPathSuffix = "openapi.yml"; | ||
public async Task GenerateManifestAsync(CancellationToken cancellationToken = default) | ||
{ | ||
// 1. cleanup any namings to be used later on. | ||
Configuration.ClientClassName = PluginNameCleanupRegex().Replace(Configuration.ClientClassName, string.Empty); //drop any special characters | ||
Configuration.ClientClassName = | ||
PluginNameCleanupRegex().Replace(Configuration.ClientClassName, string.Empty); //drop any special characters | ||
// 2. write the OpenApi description | ||
var descriptionRelativePath = $"{Configuration.ClientClassName.ToLowerInvariant()}-{DescriptionPathSuffix}"; | ||
var descriptionFullPath = Path.Combine(Configuration.OutputPath, descriptionRelativePath); | ||
|
@@ -94,6 +99,7 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de | |
default: | ||
throw new NotImplementedException($"The {pluginType} plugin is not implemented."); | ||
} | ||
|
||
await writer.FlushAsync(cancellationToken).ConfigureAwait(false); | ||
} | ||
} | ||
|
@@ -119,7 +125,9 @@ private OpenApiDocument GetDocumentWithTrimmedComponentsAndResponses(OpenApiDocu | |
var basePath = doc.GetAPIRootUrl(Configuration.OpenAPIFilePath); | ||
foreach (var path in doc.Paths.Where(static path => path.Value.Operations.Count > 0)) | ||
{ | ||
var key = string.IsNullOrEmpty(basePath) ? path.Key : $"{basePath}/{path.Key.TrimStart(KiotaBuilder.ForwardSlash)}"; | ||
var key = string.IsNullOrEmpty(basePath) | ||
? path.Key | ||
: $"{basePath}/{path.Key.TrimStart(KiotaBuilder.ForwardSlash)}"; | ||
requestUrls[key] = path.Value.Operations.Keys.Select(static key => key.ToString().ToUpperInvariant()).ToList(); | ||
} | ||
|
||
|
@@ -129,7 +137,7 @@ private OpenApiDocument GetDocumentWithTrimmedComponentsAndResponses(OpenApiDocu | |
|
||
private PluginManifestDocument GetManifestDocument(string openApiDocumentPath) | ||
{ | ||
var (runtimes, functions) = GetRuntimesAndFunctionsFromTree(TreeNode, openApiDocumentPath); | ||
var (runtimes, functions) = GetRuntimesAndFunctionsFromTree(OAIDocument, Configuration.PluginAuthInformation, TreeNode, openApiDocumentPath); | ||
var descriptionForHuman = OAIDocument.Info?.Description.CleanupXMLString() is string d && !string.IsNullOrEmpty(d) ? d : $"Description for {OAIDocument.Info?.Title.CleanupXMLString()}"; | ||
var manifestInfo = ExtractInfoFromDocument(OAIDocument.Info); | ||
return new PluginManifestDocument | ||
|
@@ -184,79 +192,119 @@ descriptionExtension is OpenApiDescriptionForModelExtension extension && | |
privacyUrl = privacy.Privacy; | ||
|
||
return new OpenApiManifestInfo(descriptionForModel, legalUrl, logoUrl, privacyUrl, contactEmail); | ||
|
||
} | ||
|
||
private const string DefaultContactName = "publisher-name"; | ||
private const string DefaultContactEmail = "[email protected]"; | ||
private sealed record OpenApiManifestInfo(string? DescriptionForModel = null, string? LegalUrl = null, string? LogoUrl = null, string? PrivacyUrl = null, string ContactEmail = DefaultContactEmail); | ||
private static (OpenApiRuntime[], Function[]) GetRuntimesAndFunctionsFromTree(OpenApiUrlTreeNode currentNode, string openApiDocumentPath) | ||
|
||
private sealed record OpenApiManifestInfo( | ||
string? DescriptionForModel = null, | ||
string? LegalUrl = null, | ||
string? LogoUrl = null, | ||
string? PrivacyUrl = null, | ||
string ContactEmail = DefaultContactEmail); | ||
|
||
private static (OpenApiRuntime[], Function[]) GetRuntimesAndFunctionsFromTree(OpenApiDocument document, PluginAuthConfiguration? authInformation, OpenApiUrlTreeNode currentNode, | ||
string openApiDocumentPath) | ||
{ | ||
var runtimes = new List<OpenApiRuntime>(); | ||
var functions = new List<Function>(); | ||
var configAuth = authInformation?.ToPluginManifestAuth(); | ||
if (currentNode.PathItems.TryGetValue(Constants.DefaultOpenApiLabel, out var pathItem)) | ||
{ | ||
foreach (var operation in pathItem.Operations.Values.Where(static x => !string.IsNullOrEmpty(x.OperationId))) | ||
{ | ||
runtimes.Add(new OpenApiRuntime | ||
{ | ||
Auth = new AnonymousAuth(), | ||
Spec = new OpenApiRuntimeSpec() | ||
{ | ||
Url = openApiDocumentPath, | ||
}, | ||
// Configuration overrides document information | ||
Auth = configAuth ?? GetAuth(operation.Security ?? document.SecurityRequirements), | ||
Spec = new OpenApiRuntimeSpec { Url = openApiDocumentPath, }, | ||
RunForFunctions = [operation.OperationId] | ||
}); | ||
functions.Add(new Function | ||
{ | ||
Name = operation.OperationId, | ||
Description = | ||
operation.Summary.CleanupXMLString() is string summary && !string.IsNullOrEmpty(summary) | ||
operation.Summary.CleanupXMLString() is { } summary && !string.IsNullOrEmpty(summary) | ||
? summary | ||
: operation.Description.CleanupXMLString(), | ||
States = GetStatesFromOperation(operation), | ||
}); | ||
} | ||
} | ||
|
||
foreach (var node in currentNode.Children) | ||
{ | ||
var (childRuntimes, childFunctions) = GetRuntimesAndFunctionsFromTree(node.Value, openApiDocumentPath); | ||
var (childRuntimes, childFunctions) = GetRuntimesAndFunctionsFromTree(document, authInformation, node.Value, openApiDocumentPath); | ||
runtimes.AddRange(childRuntimes); | ||
functions.AddRange(childFunctions); | ||
} | ||
|
||
return (runtimes.ToArray(), functions.ToArray()); | ||
} | ||
private static States? GetStatesFromOperation(OpenApiOperation openApiOperation) | ||
|
||
private static Auth GetAuth(IList<OpenApiSecurityRequirement> securityRequirements) | ||
{ | ||
return (GetStateFromExtension<OpenApiAiReasoningInstructionsExtension>(openApiOperation, OpenApiAiReasoningInstructionsExtension.Name, static x => x.ReasoningInstructions), | ||
GetStateFromExtension<OpenApiAiRespondingInstructionsExtension>(openApiOperation, OpenApiAiRespondingInstructionsExtension.Name, static x => x.RespondingInstructions)) switch | ||
// Only one security object is allowed | ||
var security = securityRequirements.SingleOrDefault(); | ||
var opSecurity = security?.Keys.SingleOrDefault(); | ||
return (opSecurity is null || opSecurity.UnresolvedReference) ? new AnonymousAuth() : GetAuthFromSecurityScheme(opSecurity); | ||
} | ||
|
||
private static Auth GetAuthFromSecurityScheme(OpenApiSecurityScheme securityScheme) | ||
{ | ||
string name = securityScheme.Reference.Id; | ||
return securityScheme.Type switch | ||
{ | ||
(State reasoning, State responding) => new States | ||
SecuritySchemeType.ApiKey => new ApiKeyPluginVault | ||
{ | ||
Reasoning = reasoning, | ||
Responding = responding | ||
ReferenceId = $"{{{name}_REGISTRATION_ID}}" | ||
}, | ||
(State reasoning, _) => new States | ||
// Only Http bearer is supported | ||
SecuritySchemeType.Http when securityScheme.Scheme.Equals("bearer", StringComparison.OrdinalIgnoreCase) => | ||
new ApiKeyPluginVault { ReferenceId = $"{{{name}_REGISTRATION_ID}}" }, | ||
SecuritySchemeType.OpenIdConnect => new ApiKeyPluginVault | ||
{ | ||
Reasoning = reasoning | ||
ReferenceId = $"{{{name}_REGISTRATION_ID}}" | ||
}, | ||
(_, State responding) => new States | ||
SecuritySchemeType.OAuth2 => new OAuthPluginVault | ||
{ | ||
Responding = responding | ||
ReferenceId = $"{{{name}_CONFIGURATION_ID}}" | ||
}, | ||
_ => throw new UnsupportedSecuritySchemeException(["Bearer Token", "Api Key", "OpenId Connect", "OAuth"], | ||
$"Unsupported security scheme type '{securityScheme.Type}'.") | ||
}; | ||
} | ||
|
||
private static States? GetStatesFromOperation(OpenApiOperation openApiOperation) | ||
{ | ||
return ( | ||
GetStateFromExtension<OpenApiAiReasoningInstructionsExtension>(openApiOperation, | ||
OpenApiAiReasoningInstructionsExtension.Name, static x => x.ReasoningInstructions), | ||
GetStateFromExtension<OpenApiAiRespondingInstructionsExtension>(openApiOperation, | ||
OpenApiAiRespondingInstructionsExtension.Name, static x => x.RespondingInstructions)) switch | ||
{ | ||
(State reasoning, State responding) => new States { Reasoning = reasoning, Responding = responding }, | ||
(State reasoning, _) => new States { Reasoning = reasoning }, | ||
(_, State responding) => new States { Responding = responding }, | ||
_ => null | ||
}; | ||
} | ||
private static State? GetStateFromExtension<T>(OpenApiOperation openApiOperation, string extensionName, Func<T, List<string>> instructionsExtractor) | ||
|
||
private static State? GetStateFromExtension<T>(OpenApiOperation openApiOperation, string extensionName, | ||
Func<T, List<string>> instructionsExtractor) | ||
{ | ||
if (openApiOperation.Extensions.TryGetValue(extensionName, out var rExtRaw) && | ||
rExtRaw is T rExt && | ||
instructionsExtractor(rExt).Exists(static x => !string.IsNullOrEmpty(x))) | ||
{ | ||
return new State | ||
{ | ||
Instructions = new Instructions(instructionsExtractor(rExt).Where(static x => !string.IsNullOrEmpty(x)).Select(static x => x.CleanupXMLString()).ToList()) | ||
Instructions = new Instructions(instructionsExtractor(rExt) | ||
.Where(static x => !string.IsNullOrEmpty(x)).Select(static x => x.CleanupXMLString()).ToList()) | ||
}; | ||
} | ||
|
||
return null; | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
src/Kiota.Builder/Plugins/UnsupportedSecuritySchemeException.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
using System; | ||
|
||
namespace Kiota.Builder.Plugins; | ||
|
||
public class UnsupportedSecuritySchemeException(string[] supportedTypes, string? message, Exception? innerException) | ||
: Exception(message, innerException) | ||
{ | ||
#pragma warning disable CA1819 | ||
public string[] SupportedTypes => supportedTypes; | ||
#pragma warning restore CA1819 | ||
|
||
public UnsupportedSecuritySchemeException(string[] supportedTypes, string? message) : this(supportedTypes, message, | ||
null) | ||
{ | ||
} | ||
|
||
public UnsupportedSecuritySchemeException() : this(null) | ||
{ | ||
} | ||
|
||
public UnsupportedSecuritySchemeException(string? message) : this(message, null) | ||
{ | ||
} | ||
|
||
public UnsupportedSecuritySchemeException(string? message, Exception? innerException) : this([], message, innerException) | ||
{ | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.