Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add auth into plugin generation #5209

Merged
merged 26 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9c65aed
add auth into plugin generation
calebkiage Aug 21, 2024
cadf0a6
add http bearer security
calebkiage Aug 27, 2024
7f5ae8a
Merge branch 'main' into caleb/feat/plugin-manifest-security
calebkiage Aug 27, 2024
d4b87a5
fix compile error
calebkiage Aug 27, 2024
4830939
refactor exception to its own file
calebkiage Aug 27, 2024
11e8539
Merge branch 'main' into caleb/feat/plugin-manifest-security
calebkiage Aug 27, 2024
3d59b14
fix security scheme reference id generation
calebkiage Aug 27, 2024
ea39bd3
Merge remote-tracking branch 'origin/caleb/feat/plugin-manifest-secur…
calebkiage Aug 27, 2024
d3bf0ee
update tests to check for the root security object.
calebkiage Aug 27, 2024
6623fc1
allow configuring plugin manifest's auth
calebkiage Aug 28, 2024
da463c9
apply formatter
calebkiage Aug 28, 2024
71d13fb
Merge branch 'main' into caleb/feat/plugin-manifest-security
calebkiage Aug 28, 2024
f7ed0d0
add changelog entry
calebkiage Aug 28, 2024
740521c
refactor local function
calebkiage Aug 29, 2024
16a6828
Merge branch 'main' into caleb/feat/plugin-manifest-security
calebkiage Aug 29, 2024
7c94b60
fix lint error
calebkiage Aug 29, 2024
6b3a8f5
fix typo in test data
calebkiage Aug 29, 2024
0ef9ab3
update code docs
calebkiage Aug 29, 2024
2d22d2e
fix issue with incorrect auth type when security schemes component do…
calebkiage Aug 29, 2024
e91fa1e
add auth information to workspace management
calebkiage Aug 30, 2024
efa783f
Merge branch 'main' into caleb/feat/plugin-manifest-security
calebkiage Aug 30, 2024
0cb928b
fix format
calebkiage Aug 30, 2024
34daa16
fix test failure
calebkiage Aug 30, 2024
4bb63a2
update hashcode
calebkiage Aug 30, 2024
1c57b25
improve hash calculation
calebkiage Aug 30, 2024
8b9434d
Merge branch 'main' into caleb/feat/plugin-manifest-security
calebkiage Aug 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}'")

Check warning on line 44 in src/Kiota.Builder/Configuration/PluginAuthConfiguration.cs

View workflow job for this annotation

GitHub Actions / Build

The parameter name 'AuthType' is not declared in the argument list. (https://rules.sonarsource.com/csharp/RSPEC-3928)

Check warning on line 44 in src/Kiota.Builder/Configuration/PluginAuthConfiguration.cs

View workflow job for this annotation

GitHub Actions / Build

The parameter name 'AuthType' is not declared in the argument list. (https://rules.sonarsource.com/csharp/RSPEC-3928)
};
}
}
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 @@
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 @@ -76,7 +81,7 @@
pluginDocument.Write(writer);
break;
case PluginType.APIManifest:
var apiManifest = new ApiManifestDocument("application"); //TODO add application name

Check warning on line 84 in src/Kiota.Builder/Plugins/PluginsGenerationService.cs

View workflow job for this annotation

GitHub Actions / Build

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)

Check warning on line 84 in src/Kiota.Builder/Plugins/PluginsGenerationService.cs

View workflow job for this annotation

GitHub Actions / Build

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)
// pass empty config hash so that its not included in this manifest.
apiManifest.ApiDependencies.AddOrReplace(Configuration.ClientClassName, Configuration.ToApiDependency(string.Empty, TreeNode?.GetRequestInfo().ToDictionary(static x => x.Key, static x => x.Value) ?? [], WorkingDirectory));
var publisherName = string.IsNullOrEmpty(OAIDocument.Info?.Contact?.Name)
Expand All @@ -94,6 +99,7 @@
default:
throw new NotImplementedException($"The {pluginType} plugin is not implemented.");
}

await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
}
}
Expand All @@ -119,7 +125,9 @@
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 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 @@
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
Loading