Skip to content

Commit

Permalink
add auth into plugin generation (#5209)
Browse files Browse the repository at this point in the history
* 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
calebkiage authored Aug 30, 2024
1 parent 66b5a23 commit 76fa86f
Show file tree
Hide file tree
Showing 10 changed files with 542 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Added the ability to export the CodeDom to a file showing the public APIs to be generated in a given language [#4627](https://github.com/microsoft/kiota/issues/4627)
- Added composed type serialization in Typescript [2462](https://github.com/microsoft/kiota/issues/2462)
- Use authentication information available in the source OpenAPI document when generating a plugin manifest. [#5070](https://github.com/microsoft/kiota/issues/5070)

### Changed

Expand Down
10 changes: 10 additions & 0 deletions src/Kiota.Builder/Configuration/GenerationConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.OpenApi.ApiManifest;

namespace Kiota.Builder.Configuration;

#pragma warning disable CA2227
#pragma warning disable CA1056
public class GenerationConfiguration : ICloneable
Expand Down Expand Up @@ -157,6 +158,7 @@ public object Clone()
PluginTypes = new(PluginTypes ?? Enumerable.Empty<PluginType>()),
DisableSSLValidation = DisableSSLValidation,
ExportPublicApi = ExportPublicApi,
PluginAuthInformation = PluginAuthInformation,
};
}
private static readonly StringIEnumerableDeepComparer comparer = new();
Expand Down Expand Up @@ -211,6 +213,14 @@ public bool DisableSSLValidation
{
get; set;
}

/// <summary>
/// Authentication information to be used when generating the plugin manifest.
/// </summary>
public PluginAuthConfiguration? PluginAuthInformation
{
get; set;
}
}
#pragma warning restore CA1056
#pragma warning restore CA2227
47 changes: 47 additions & 0 deletions src/Kiota.Builder/Configuration/PluginAuthConfiguration.cs
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}'")
};
}
}
16 changes: 16 additions & 0 deletions src/Kiota.Builder/Configuration/PluginAuthType.cs
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
}
100 changes: 74 additions & 26 deletions src/Kiota.Builder/Plugins/PluginsGenerationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
}
Expand All @@ -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();
}

Expand All @@ -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
Expand Down Expand Up @@ -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 src/Kiota.Builder/Plugins/UnsupportedSecuritySchemeException.cs
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)
{
}
}
25 changes: 25 additions & 0 deletions src/Kiota.Builder/WorkspaceManagement/ApiPluginConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using Kiota.Builder.Configuration;
using Microsoft.OpenApi.ApiManifest;
using Microsoft.Plugins.Manifest;

namespace Kiota.Builder.WorkspaceManagement;

Expand All @@ -24,12 +25,29 @@ public ApiPluginConfiguration(GenerationConfiguration config) : base(config)
{
ArgumentNullException.ThrowIfNull(config);
Types = config.PluginTypes.Select(x => x.ToString()).ToHashSet(StringComparer.OrdinalIgnoreCase);
AuthType = config.PluginAuthInformation?.AuthType.ToString();
AuthReferenceId = config.PluginAuthInformation?.ReferenceId;
}
public HashSet<string> Types { get; set; } = new(StringComparer.OrdinalIgnoreCase);

public string? AuthType
{
get;
set;
}

public string? AuthReferenceId
{
get;
set;
}

public object Clone()
{
var result = new ApiPluginConfiguration()
{
AuthType = AuthType,
AuthReferenceId = AuthReferenceId,
Types = new HashSet<string>(Types, StringComparer.OrdinalIgnoreCase)
};
CloneBase(result);
Expand All @@ -46,6 +64,13 @@ public void UpdateGenerationConfigurationFromApiPluginConfiguration(GenerationCo
ArgumentNullException.ThrowIfNull(config);
ArgumentException.ThrowIfNullOrEmpty(pluginName);
config.PluginTypes = Types.Select(x => Enum.TryParse<PluginType>(x, true, out var result) ? result : (PluginType?)null).OfType<PluginType>().ToHashSet();
if (AuthReferenceId is not null && Enum.TryParse<PluginAuthType>(AuthType, out var authType))
{
config.PluginAuthInformation = new PluginAuthConfiguration(AuthReferenceId)
{
AuthType = authType,
};
}
UpdateGenerationConfigurationFromBase(config, pluginName, requests);
}
}
Expand Down
Loading

0 comments on commit 76fa86f

Please sign in to comment.