From 0ef4ed0988d9fa2d10c0caf198d34077200c7b2f Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sun, 21 May 2023 15:36:27 -0400 Subject: [PATCH 1/7] Added Azure Pipeline --- .azure-pipelines/ci-build.yml | 319 ++++++++++++++++++++++++++++++++++ src/lib/apimanifest.csproj | 4 + 2 files changed, 323 insertions(+) create mode 100644 .azure-pipelines/ci-build.yml diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml new file mode 100644 index 0000000..9ecc38c --- /dev/null +++ b/.azure-pipelines/ci-build.yml @@ -0,0 +1,319 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +name: $(BuildDefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) + +trigger: + branches: + include: + - main +pr: + branches: + include: + - main + +pool: + name: Azure Pipelines + vmImage: windows-latest + +variables: + buildPlatform: 'Any CPU' + buildConfiguration: 'Release' + ProductBinPath: '$(Build.SourcesDirectory)\src\bin\$(BuildConfiguration)' + + +stages: + +- stage: build + jobs: + - job: build + steps: + + - task: UseDotNet@2 + displayName: 'Use .NET 7' + inputs: + version: 7.x + + - task: UseDotNet@2 + displayName: 'Use .NET 6 (for code signing tasks)' + inputs: + packageType: sdk + version: 6.x + + - task: PoliCheck@2 + displayName: 'Run PoliCheck "/src"' + inputs: + inputType: CmdLine + cmdLineArgs: '/F:$(Build.SourcesDirectory)/src /T:9 /Sev:"1|2" /PE:2 /O:poli_result_src.xml' + + - task: PoliCheck@2 + displayName: 'Run PoliCheck "/src/tests"' + inputs: + inputType: CmdLine + cmdLineArgs: '/F:$(Build.SourcesDirectory)/src/tests /T:9 /Sev:"1|2" /PE:2 /O:poli_result_test.xml' + + # Install the nuget tool. + - task: NuGetToolInstaller@1 + displayName: 'Install Nuget dependency manager' + inputs: + versionSpec: '>=5.2.0' + checkLatest: true + + - task: PowerShell@2 + displayName: 'Enable signing' + inputs: + targetType: filePath + filePath: 'scripts\EnableSigning.ps1' + arguments: '-projectPath "$(Build.SourcesDirectory)/src/lib/apimanifest.csproj"' + pwsh: true + enabled: true + + - task: PowerShell@2 + displayName: 'Enable signing' + inputs: + targetType: filePath + filePath: 'scripts\EnableSigning.ps1' + arguments: '-projectPath "$(Build.SourcesDirectory)/src/tests/tests.csproj"' + pwsh: true + enabled: true + + # Build the Product project + - task: DotNetCoreCLI@2 + displayName: 'Build Microsoft.Kiota.Http.HttpClientLibrary' + inputs: + projects: '$(Build.SourcesDirectory)\apimanifest.sln' + arguments: '--configuration $(BuildConfiguration) --no-incremental' + + # Run the Unit test + - task: DotNetCoreCLI@2 + displayName: 'Test Microsoft.Kiota.Http.HttpClientLibrary' + inputs: + command: test + projects: '$(Build.SourcesDirectory)\apimanifest.sln' + arguments: '--configuration $(BuildConfiguration) --no-build -f net7.0' + + # CredScan + - task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@3 + displayName: 'Run CredScan - Src' + inputs: + toolMajorVersion: 'V2' + scanFolder: '$(Build.SourcesDirectory)\src' + debugMode: false + + - task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@3 + displayName: 'Run CredScan - Test' + inputs: + toolMajorVersion: 'V2' + scanFolder: '$(Build.SourcesDirectory)\src\tests' + debugMode: false + + - task: AntiMalware@3 + displayName: 'Run MpCmdRun.exe - ProductBinPath' + inputs: + FileDirPath: '$(ProductBinPath)' + enabled: false + + - task: BinSkim@4 + displayName: 'Run BinSkim - Product Binaries' + inputs: + InputType: Basic + AnalyzeTargetGlob: '$(ProductBinPath)\**\Microsoft.OpenApi.ApiManifest.dll' + AnalyzeSymPath: '$(ProductBinPath)' + AnalyzeVerbose: true + AnalyzeHashes: true + AnalyzeEnvironment: true + + - task: PublishSecurityAnalysisLogs@3 + displayName: 'Publish Security Analysis Logs' + inputs: + ArtifactName: SecurityLogs + + - task: PostAnalysis@2 + displayName: 'Post Analysis' + inputs: + BinSkim: true + CredScan: true + PoliCheck: true + + - task: EsrpCodeSigning@2 + displayName: 'ESRP DLL Strong Name' + inputs: + ConnectedServiceName: 'microsoftgraph ESRP CodeSign DLL and NuGet (AKV)' + FolderPath: $(ProductBinPath) + UseMinimatch: true + Pattern: '**\*Microsoft.OpenApi.ApiManifest.dll' + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "keyCode": "CP-233863-SN", + "operationSetCode": "StrongNameSign", + "parameters": [], + "toolName": "sign", + "toolVersion": "1.0" + }, + { + "keyCode": "CP-233863-SN", + "operationSetCode": "StrongNameVerify", + "parameters": [], + "toolName": "sign", + "toolVersion": "1.0" + } + ] + SessionTimeout: 20 + + - task: EsrpCodeSigning@2 + displayName: 'ESRP DLL CodeSigning' + inputs: + ConnectedServiceName: 'microsoftgraph ESRP CodeSign DLL and NuGet (AKV)' + FolderPath: src + Pattern: '**\*Microsoft.OpenApi.ApiManifest.dll' + UseMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "keyCode": "CP-230012", + "operationSetCode": "SigntoolSign", + "parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "Microsoft" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "http://www.microsoft.com" + }, + { + "parameterName": "FileDigest", + "parameterValue": "/fd \"SHA256\"" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + ], + "toolName": "sign", + "toolVersion": "1.0" + }, + { + "keyCode": "CP-230012", + "operationSetCode": "SigntoolVerify", + "parameters": [ ], + "toolName": "sign", + "toolVersion": "1.0" + } + ] + SessionTimeout: 20 + + # arguments are not parsed in DotNetCoreCLI@2 task for `pack` command, that's why we have a custom pack command here + - pwsh: dotnet pack $env:BUILD_SOURCESDIRECTORY/src/lib/apimanifest.csproj /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg --no-build --output $env:BUILD_ARTIFACTSTAGINGDIRECTORY --configuration $env:BUILD_CONFIGURATION + env: + BUILD_CONFIGURATION: $(BuildConfiguration) + displayName: Dotnet pack + + - task: PowerShell@2 + displayName: 'Validate project version has been incremented' + condition: and(contains(variables['build.sourceBranch'], 'refs/heads/main'), succeeded()) + inputs: + targetType: 'filePath' + filePath: $(System.DefaultWorkingDirectory)\scripts\ValidateProjectVersionUpdated.ps1 + arguments: '-projectPath "$(Build.SourcesDirectory)/src/lib/apimanifest.csproj" -packageName "Microsoft.OpenApi.ApiManifest"' + pwsh: true + + - task: EsrpCodeSigning@2 + displayName: 'ESRP CodeSigning Nuget Packages' + inputs: + ConnectedServiceName: 'microsoftgraph ESRP CodeSign DLL and NuGet (AKV)' + FolderPath: '$(Build.ArtifactStagingDirectory)' + UseMinimatch: true + Pattern: '*.nupkg' + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "keyCode": "CP-401405", + "operationSetCode": "NuGetSign", + "parameters": [ ], + "toolName": "sign", + "toolVersion": "1.0" + }, + { + "keyCode": "CP-401405", + "operationSetCode": "NuGetVerify", + "parameters": [ ], + "toolName": "sign", + "toolVersion": "1.0" + } + ] + SessionTimeout: 20 + + - task: CopyFiles@2 + displayName: 'Copy release scripts to artifact staging directory' + condition: and(contains(variables['build.sourceBranch'], 'refs/heads/main'), succeeded()) + inputs: + SourceFolder: '$(Build.SourcesDirectory)' + Contents: 'scripts\**' + TargetFolder: '$(Build.ArtifactStagingDirectory)' + + - task: PublishPipelineArtifact@1 + displayName: 'Upload Artifact: Nugets' + inputs: + artifactName: Nugets + targetPath: $(Build.ArtifactStagingDirectory) + +- stage: deploy + condition: and(contains(variables['build.sourceBranch'], 'refs/heads/main'), succeeded()) + dependsOn: build + jobs: + - deployment: deploy_openapi_apimanifest + dependsOn: [] + environment: nuget-org + strategy: + runOnce: + deploy: + pool: + vmImage: ubuntu-latest + steps: + # Install the nuget tool. + - task: NuGetToolInstaller@0 + displayName: 'Use NuGet >=5.2.0' + inputs: + versionSpec: '>=5.2.0' + checkLatest: true + - task: DownloadPipelineArtifact@2 + displayName: Download nupkg from artifacts + inputs: + artifact: Nugets + source: current + - task: PowerShell@2 + displayName: 'Extract release information to pipeline' + inputs: + targetType: 'filePath' + filePath: $(Pipeline.Workspace)\Nugets\scripts\GetNugetPackageVersion.ps1 + pwsh: true + arguments: '-packageDirPath "$(Pipeline.Workspace)/Nugets/"' + - task: NuGetCommand@2 + displayName: 'NuGet push' + inputs: + command: push + packagesToPush: '$(Pipeline.Workspace)/Nugets/Microsoft.OpenApi.ApiManifest.nupkg' + nuGetFeedType: external + publishFeedCredentials: 'Kiota Nuget Connection' + - task: GitHubRelease@1 + displayName: 'GitHub release (create)' + inputs: + gitHubConnection: 'Kiota_Release' + target: $(Build.SourceVersion) + tagSource: userSpecifiedTag + tag: 'v$(VERSION_STRING)' + title: '$(VERSION_STRING)' + releaseNotesSource: inline + assets: '!**/**' + changeLogType: issueBased + isPreRelease : '$(IS_PRE_RELEASE)' + addChangeLog : true \ No newline at end of file diff --git a/src/lib/apimanifest.csproj b/src/lib/apimanifest.csproj index ac543b7..e008e02 100644 --- a/src/lib/apimanifest.csproj +++ b/src/lib/apimanifest.csproj @@ -14,6 +14,10 @@ ./../../artifacts enable enable + True + API Manifest + Microsoft.OpenApi.ApiManifest + Microsoft.OpenApi.ApiManifest From 727480fab8e65ac1926ae20181e8df38e684dfce Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sun, 21 May 2023 15:37:46 -0400 Subject: [PATCH 2/7] Fixed a display issues --- .azure-pipelines/ci-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 9ecc38c..89b1b8d 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -79,7 +79,7 @@ stages: # Build the Product project - task: DotNetCoreCLI@2 - displayName: 'Build Microsoft.Kiota.Http.HttpClientLibrary' + displayName: 'Build APIManifest' inputs: projects: '$(Build.SourcesDirectory)\apimanifest.sln' arguments: '--configuration $(BuildConfiguration) --no-incremental' From 6d1f80a75b1f41a9d41369a4f2cbadd84ca120ac Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sun, 21 May 2023 15:38:27 -0400 Subject: [PATCH 3/7] Fixed more display issues --- .azure-pipelines/ci-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 89b1b8d..7bcb55f 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -86,7 +86,7 @@ stages: # Run the Unit test - task: DotNetCoreCLI@2 - displayName: 'Test Microsoft.Kiota.Http.HttpClientLibrary' + displayName: 'Test Microsoft.OpenApi.ApiManifest' inputs: command: test projects: '$(Build.SourcesDirectory)\apimanifest.sln' From 80a7624db01c6253938c5eb6423ec13de0402e55 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sun, 21 May 2023 16:24:06 -0400 Subject: [PATCH 4/7] Changed to strong name with checked in key --- .azure-pipelines/ci-build.yml | 45 ---------------------------------- src/lib/apimanifest.csproj | 1 + src/lib/sgKey.snk | Bin 0 -> 596 bytes 3 files changed, 1 insertion(+), 45 deletions(-) create mode 100644 src/lib/sgKey.snk diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 7bcb55f..93de575 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -59,24 +59,6 @@ stages: versionSpec: '>=5.2.0' checkLatest: true - - task: PowerShell@2 - displayName: 'Enable signing' - inputs: - targetType: filePath - filePath: 'scripts\EnableSigning.ps1' - arguments: '-projectPath "$(Build.SourcesDirectory)/src/lib/apimanifest.csproj"' - pwsh: true - enabled: true - - - task: PowerShell@2 - displayName: 'Enable signing' - inputs: - targetType: filePath - filePath: 'scripts\EnableSigning.ps1' - arguments: '-projectPath "$(Build.SourcesDirectory)/src/tests/tests.csproj"' - pwsh: true - enabled: true - # Build the Product project - task: DotNetCoreCLI@2 displayName: 'Build APIManifest' @@ -135,33 +117,6 @@ stages: CredScan: true PoliCheck: true - - task: EsrpCodeSigning@2 - displayName: 'ESRP DLL Strong Name' - inputs: - ConnectedServiceName: 'microsoftgraph ESRP CodeSign DLL and NuGet (AKV)' - FolderPath: $(ProductBinPath) - UseMinimatch: true - Pattern: '**\*Microsoft.OpenApi.ApiManifest.dll' - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "keyCode": "CP-233863-SN", - "operationSetCode": "StrongNameSign", - "parameters": [], - "toolName": "sign", - "toolVersion": "1.0" - }, - { - "keyCode": "CP-233863-SN", - "operationSetCode": "StrongNameVerify", - "parameters": [], - "toolName": "sign", - "toolVersion": "1.0" - } - ] - SessionTimeout: 20 - - task: EsrpCodeSigning@2 displayName: 'ESRP DLL CodeSigning' inputs: diff --git a/src/lib/apimanifest.csproj b/src/lib/apimanifest.csproj index e008e02..601285e 100644 --- a/src/lib/apimanifest.csproj +++ b/src/lib/apimanifest.csproj @@ -18,6 +18,7 @@ API Manifest Microsoft.OpenApi.ApiManifest Microsoft.OpenApi.ApiManifest + sgKey.snk diff --git a/src/lib/sgKey.snk b/src/lib/sgKey.snk new file mode 100644 index 0000000000000000000000000000000000000000..4752103d753e8a32bc2a8fb119dada64eb1ebbbd GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50097b6dM^8J~;m<8l%s$o``%5C$Ej9kM&F2 zP&Q7Z9NJN_Wv$0{^+#ycv5OiN?r~@6zWT4w*q*F7c!(gN&$IQ0f4oM=`tMxv(e?lY zko2H<6A>2;r1cwH7UcSTPbZd1mQ;=YR$az>SSi@t0x9o(q^e{@yHA-WtR@8G-p{iYF`^9+;eW$d#ICwlT71PzG0uC{q)0 zKWfV5nSw2Qe$?(zVN0md160b!CXwFLt{ZyA)C#{O_iqWggmmmMy>|HXOz&>{xO4}g zk17_Vzqim8t^NH<{$Ys==$^y+kv&>lRiuvwh!(~%tKvF#F*D0nfaRr8?AM-T2-*`0QRbTjv2M6)ee~#V z&EnKC((~8Bn~){WcuWz7CBW`s=J Date: Sun, 21 May 2023 16:31:55 -0400 Subject: [PATCH 5/7] Fixed the binskim path --- .azure-pipelines/ci-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 93de575..700fe02 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -19,7 +19,7 @@ pool: variables: buildPlatform: 'Any CPU' buildConfiguration: 'Release' - ProductBinPath: '$(Build.SourcesDirectory)\src\bin\$(BuildConfiguration)' + ProductBinPath: '$(Build.SourcesDirectory)\src\lib\bin\$(BuildConfiguration)' stages: From 0f9f82b0048e9e9c0396d402939576901bd7a84e Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sun, 21 May 2023 17:37:32 -0400 Subject: [PATCH 6/7] Fixed examples and tests for JSON content --- README.md | 55 +++++++++++++++++++++------------------- src/lib/ApiDependency.cs | 4 --- src/tests/BasicTests.cs | 16 +++++++++--- 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 160eedc..bcf8461 100644 --- a/README.md +++ b/README.md @@ -7,32 +7,35 @@ An "api manifest" is a way to store the dependencies that an application has on You can create an API manifest in code: ```csharp - var apiManifest = new ApiManifestDocument(); -apiManifest.Publisher = new Publisher(); -apiManifest.Publisher.Name = "Microsoft"; -apiManifest.Publisher.ContactEmail = "example@example.org"; -apiManifest.ApiDependencies.Add(new ApiDependency() -{ - ApiDescripionUrl = "https://example.org", - Auth = new Auth() - { - ClientId = "1234", - Permissions = new() { - {"application", new() {"read"}}, - {"delegated", new() {"read", "write"}} - } - }, - Requests = new List() { - new() { - Method = "GET", - UriTemplate = "/api/v1/endpoint" - }, - new () { - Method = "POST", - UriTemplate = "/api/v1/endpoint" - } - } -}); + var apiManifest = new ApiManifestDocument() { + Publisher = new() { + Name = "Microsoft", + ContactEmail = "example@example.org" + }, + ApiDependencies = new() { + { "example", new() + { + ApiDescripionUrl = "https://example.org", + Auth = new() + { + ClientIdentifier = "1234", + Access = new() { + new () { Type= "application", Content = new JsonObject() { + { "scopes", new JsonArray() {"User.Read.All"} }} + } , + new () { Type= "delegated", Content = new JsonObject() { + { "scopes", new JsonArray() {"User.Read", "Mail.Read"} }} + } + } + }, + Requests = new() { + new() { Method = "GET", UriTemplate = "/api/v1/endpoint" }, + new () { Method = "POST", UriTemplate = "/api/v1/endpoint"} + } + } + } + } + }; ``` or you can read it from a stream diff --git a/src/lib/ApiDependency.cs b/src/lib/ApiDependency.cs index 0c01cbb..9177e25 100644 --- a/src/lib/ApiDependency.cs +++ b/src/lib/ApiDependency.cs @@ -4,14 +4,12 @@ namespace Microsoft.OpenApi.ApiManifest; public class ApiDependency { public string? ApiDescripionUrl { get; set; } - public string? BaseUrl { get; set; } public Auth? Auth { get; set; } public List Requests { get; set; } = new List(); private const string ApiDescriptionUrlProperty = "apiDescripionUrl"; private const string AuthProperty = "auth"; private const string RequestsProperty = "requests"; - private const string BaseUrlProperty = "baseUrl"; // Write method public void Write(Utf8JsonWriter writer) @@ -23,7 +21,6 @@ public void Write(Utf8JsonWriter writer) writer.WritePropertyName(AuthProperty); Auth?.Write(writer); } - if (!String.IsNullOrWhiteSpace(BaseUrl)) writer.WriteString(BaseUrlProperty, BaseUrl); if (Requests.Count > 0) { writer.WritePropertyName(RequestsProperty); @@ -43,7 +40,6 @@ public void Write(Utf8JsonWriter writer) { ApiDescriptionUrlProperty, (o,v) => {o.ApiDescripionUrl = v.GetString(); } }, { AuthProperty, (o,v) => {o.Auth = Auth.Load(v); } }, { RequestsProperty, (o,v) => {o.Requests = ParsingHelpers.GetList(v, Request.Load); } }, - { BaseUrlProperty, (o,v) => {o.BaseUrl = v.GetString(); } }, }; // Load Method diff --git a/src/tests/BasicTests.cs b/src/tests/BasicTests.cs index 33a3846..52651a6 100644 --- a/src/tests/BasicTests.cs +++ b/src/tests/BasicTests.cs @@ -50,7 +50,13 @@ public void DeserializeDocument() var json = reader.ReadToEnd(); var doc = JsonDocument.Parse(json); var apiManifest = ApiManifestDocument.Load(doc.RootElement); - Assert.Equivalent(exampleApiManifest, apiManifest ); + Assert.Equivalent(exampleApiManifest.Publisher, apiManifest.Publisher ); + Assert.Equivalent(exampleApiManifest.ApiDependencies["example"].Requests, apiManifest.ApiDependencies["example"].Requests ); + Assert.Equivalent(exampleApiManifest.ApiDependencies["example"].ApiDescripionUrl, apiManifest.ApiDependencies["example"].ApiDescripionUrl ); + var expectedAuth = exampleApiManifest.ApiDependencies["example"].Auth; + var actualAuth = apiManifest.ApiDependencies["example"].Auth; + Assert.Equivalent(expectedAuth.ClientIdentifier, actualAuth.ClientIdentifier ); + Assert.Equivalent(expectedAuth.Access[0].Content.ToJsonString(), actualAuth.Access[0].Content.ToJsonString() ); } private static ApiManifestDocument CreateDocument() @@ -68,8 +74,12 @@ private static ApiManifestDocument CreateDocument() { ClientIdentifier = "1234", Access = new() { - new () { Type= "application", Content = new JsonObject() } , - new () { Type= "delegated", Content = new JsonObject() } + new () { Type= "application", Content = new JsonObject() { + { "scopes", new JsonArray() {"User.Read.All"} }} + } , + new () { Type= "delegated", Content = new JsonObject() { + { "scopes", new JsonArray() {"User.Read", "Mail.Read"} }} + } } }, Requests = new() { From 9ef50fc987badceed0c8071d10daa2b5c4baf93b Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Mon, 29 May 2023 22:28:05 -0400 Subject: [PATCH 7/7] Added Extensions to ApiManifest --- src/lib/ApiDependency.cs | 13 +++ src/lib/ApiManifestDocument.cs | 8 ++ src/lib/Extensions.cs | 30 +++++ src/lib/OpenAI/Api.cs | 36 ++++++ src/lib/OpenAI/BaseManifestAuth.cs | 140 +++++++++++++++++++++++ src/lib/OpenAI/OpenAIPluginManifest.cs | 66 +++++++++++ src/tests/PluginTests.cs | 151 +++++++++++++++++++++++++ 7 files changed, 444 insertions(+) create mode 100644 src/lib/Extensions.cs create mode 100644 src/lib/OpenAI/Api.cs create mode 100644 src/lib/OpenAI/BaseManifestAuth.cs create mode 100644 src/lib/OpenAI/OpenAIPluginManifest.cs create mode 100644 src/tests/PluginTests.cs diff --git a/src/lib/ApiDependency.cs b/src/lib/ApiDependency.cs index 9177e25..7e1fc86 100644 --- a/src/lib/ApiDependency.cs +++ b/src/lib/ApiDependency.cs @@ -4,12 +4,16 @@ namespace Microsoft.OpenApi.ApiManifest; public class ApiDependency { public string? ApiDescripionUrl { get; set; } + public string? ApiDescripionVersion { get; set; } public Auth? Auth { get; set; } public List Requests { get; set; } = new List(); + public Extensions? Extensions { get; set; } private const string ApiDescriptionUrlProperty = "apiDescripionUrl"; + private const string ApiDescriptionVersionProperty = "apiDescripionVersion"; private const string AuthProperty = "auth"; private const string RequestsProperty = "requests"; + private const string ExtensionsProperty = "extensions"; // Write method public void Write(Utf8JsonWriter writer) @@ -17,6 +21,8 @@ public void Write(Utf8JsonWriter writer) writer.WriteStartObject(); if (!String.IsNullOrWhiteSpace(ApiDescripionUrl)) writer.WriteString(ApiDescriptionUrlProperty, ApiDescripionUrl); + if (!String.IsNullOrWhiteSpace(ApiDescripionVersion)) writer.WriteString(ApiDescriptionVersionProperty, ApiDescripionVersion); + if (Auth != null) { writer.WritePropertyName(AuthProperty); Auth?.Write(writer); @@ -32,14 +38,21 @@ public void Write(Utf8JsonWriter writer) writer.WriteEndArray(); } + if (Extensions != null) { + writer.WritePropertyName(ExtensionsProperty); + Extensions?.Write(writer); + } + writer.WriteEndObject(); } // Fixed fieldmap for ApiDependency private static FixedFieldMap handlers = new() { { ApiDescriptionUrlProperty, (o,v) => {o.ApiDescripionUrl = v.GetString(); } }, + { ApiDescriptionVersionProperty, (o,v) => {o.ApiDescripionVersion = v.GetString(); } }, { AuthProperty, (o,v) => {o.Auth = Auth.Load(v); } }, { RequestsProperty, (o,v) => {o.Requests = ParsingHelpers.GetList(v, Request.Load); } }, + { ExtensionsProperty, (o,v) => {o.Extensions = Extensions.Load(v); } } }; // Load Method diff --git a/src/lib/ApiManifestDocument.cs b/src/lib/ApiManifestDocument.cs index 1307c7d..a8e8778 100644 --- a/src/lib/ApiManifestDocument.cs +++ b/src/lib/ApiManifestDocument.cs @@ -6,9 +6,11 @@ public class ApiManifestDocument { public Publisher? Publisher { get; set; } public ApiDependencies ApiDependencies { get; set; } = new ApiDependencies(); + public Extensions Extensions { get; set; } = new Extensions(); private const string PublisherProperty = "publisher"; private const string ApiDependenciesProperty = "apiDependencies"; + private const string ExtensionsProperty = "extensions"; // Write method public void Write(Utf8JsonWriter writer) @@ -27,6 +29,11 @@ public void Write(Utf8JsonWriter writer) } writer.WriteEndObject(); + if (Extensions.Count > 0) { + writer.WritePropertyName(ExtensionsProperty); + Extensions.Write(writer); + } + writer.WriteEndObject(); } // Load method @@ -41,6 +48,7 @@ public static ApiManifestDocument Load(JsonElement value) { { PublisherProperty, (o,v) => {o.Publisher = Publisher.Load(v); } }, { ApiDependenciesProperty, (o,v) => {o.ApiDependencies = new ApiDependencies(ParsingHelpers.GetMap(v, ApiDependency.Load)); } }, + { ExtensionsProperty, (o,v) => {o.Extensions = Extensions.Load(v); } } }; } diff --git a/src/lib/Extensions.cs b/src/lib/Extensions.cs new file mode 100644 index 0000000..7b7eb0f --- /dev/null +++ b/src/lib/Extensions.cs @@ -0,0 +1,30 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Microsoft.OpenApi.ApiManifest; + +public class Extensions : Dictionary +{ + public static Extensions Load(JsonElement value) + { + var extensions = new Extensions(); + foreach(var property in value.EnumerateObject()) + { + if (property.Value.ValueKind != JsonValueKind.Null) { + extensions.Add(property.Name, JsonSerializer.Deserialize(property.Value.GetRawText())); + } + } + return extensions; + } + + public void Write(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + foreach(var extension in this) + { + writer.WritePropertyName(extension.Key); + writer.WriteRawValue(extension.Value.ToJsonString()); + } + writer.WriteEndObject(); + } +} \ No newline at end of file diff --git a/src/lib/OpenAI/Api.cs b/src/lib/OpenAI/Api.cs new file mode 100644 index 0000000..e8e94ac --- /dev/null +++ b/src/lib/OpenAI/Api.cs @@ -0,0 +1,36 @@ + +using System.Text.Json; + +namespace Microsoft.OpenApi.ApiManifest.OpenAI; + +public class Api { + public string? Type { get; set; } + public string? Url { get; set; } + public bool? IsUserAuthenticated { get; set; } + + public static Api Load(JsonElement value) + { + var api = new Api(); + ParsingHelpers.ParseMap(value, api, handlers); + return api; + } + + // Create handlers FixedFieldMap for Api + private static FixedFieldMap handlers = new() + { + { "type", (o,v) => {o.Type = v.GetString(); } }, + { "url", (o,v) => {o.Url = v.GetString(); } }, + { "is_user_authenticated", (o,v) => {o.IsUserAuthenticated = v.GetBoolean(); }}, + }; + + public void Write(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + writer.WriteString("type", Type); + writer.WriteString("url", Url); + writer.WriteBoolean("is_user_authenticated", IsUserAuthenticated ?? false); + writer.WriteEndObject(); + } +} + + diff --git a/src/lib/OpenAI/BaseManifestAuth.cs b/src/lib/OpenAI/BaseManifestAuth.cs new file mode 100644 index 0000000..6544966 --- /dev/null +++ b/src/lib/OpenAI/BaseManifestAuth.cs @@ -0,0 +1,140 @@ + +using System.Text.Json; + +namespace Microsoft.OpenApi.ApiManifest.OpenAI; + +public abstract class BaseManifestAuth +{ + public string? Type { get; set; } + public string? Instructions { get; set; } + + public static BaseManifestAuth? Load(JsonElement value) + { + BaseManifestAuth? auth = null; + + switch(value.GetProperty("type").GetString()) { + case "none": + auth = new ManifestNoAuth(); + ParsingHelpers.ParseMap(value, (ManifestNoAuth)auth, ManifestNoAuth.handlers); + break; + case "user_http": + auth = new ManifestUserHttpAuth(); + ParsingHelpers.ParseMap(value, (ManifestUserHttpAuth)auth, ManifestUserHttpAuth.handlers); + break; + case "service_http": + auth = new ManifestServiceHttpAuth(); + ParsingHelpers.ParseMap(value, (ManifestServiceHttpAuth)auth, ManifestServiceHttpAuth.handlers); + break; + case "oauth": + auth = new ManifestOAuthAuth(); + ParsingHelpers.ParseMap(value, (ManifestOAuthAuth)auth, ManifestOAuthAuth.handlers); + break; + } + + return auth; + } + + // Create handlers FixedFieldMap for ManifestAuth + + public virtual void Write(Utf8JsonWriter writer) {} + +} + +public class ManifestNoAuth : BaseManifestAuth +{ + public ManifestNoAuth() + { + Type = "none"; + } + + internal static FixedFieldMap handlers = new() + { + { "type", (o,v) => {o.Type = v.GetString(); } }, + { "instructions", (o,v) => {o.Instructions = v.GetString(); } }, + }; + + public override void Write(Utf8JsonWriter writer) { + writer.WriteStartObject(); + writer.WriteString("type", Type); + if(Instructions != null) writer.WriteString("instructions", Instructions); + writer.WriteEndObject(); + } +} + +public class ManifestOAuthAuth : BaseManifestAuth +{ + public string? ClientUrl { get; set; } + public string? Scope { get; set; } + public string? AuthorizationUrl { get; set; } + public string? AuthorizationContentType { get; set; } + public Dictionary? VerificationTokens { get; set; } + + public ManifestOAuthAuth() + { + Type = "oauth"; + } + internal static FixedFieldMap handlers = new() + { + { "type", (o,v) => {o.Type = v.GetString(); } }, + { "instructions", (o,v) => {o.Instructions = v.GetString(); } }, + { "client_url", (o,v) => {o.ClientUrl = v.GetString(); } }, + { "scope", (o,v) => {o.Scope = v.GetString(); } }, + { "authorization_url", (o,v) => {o.AuthorizationUrl = v.GetString(); } }, + { "authorization_content_type", (o,v) => {o.AuthorizationContentType = v.GetString(); } }, + { "verification_tokens", (o,v) => { o.VerificationTokens = ParsingHelpers.GetMap(v,(e) => e.GetString() ); } }, + }; + + public override void Write(Utf8JsonWriter writer) { + writer.WriteStartObject(); + writer.WriteString("type", Type); + + if(Instructions != null) writer.WriteString("instructions", Instructions); + if(ClientUrl != null) writer.WriteString("client_url", ClientUrl); + if(Scope != null) writer.WriteString("scope", Scope); + if(AuthorizationUrl != null) writer.WriteString("authorization_url", AuthorizationUrl); + if(AuthorizationContentType != null) writer.WriteString("authorization_content_type", AuthorizationContentType); + writer.WriteEndObject(); + } +} + +public class ManifestUserHttpAuth : BaseManifestAuth +{ + public ManifestUserHttpAuth() + { + Type = "user_http"; + } + internal static FixedFieldMap handlers = new() + { + { "type", (o,v) => {o.Type = v.GetString(); } }, + { "instructions", (o,v) => {o.Instructions = v.GetString(); } }, + }; + public override void Write(Utf8JsonWriter writer) { + writer.WriteStartObject(); + writer.WriteString("type", Type); + writer.WriteString("instructions", Instructions); + writer.WriteEndObject(); + } +} + +public class ManifestServiceHttpAuth : BaseManifestAuth +{ + public ManifestServiceHttpAuth() + { + Type = "service_http"; + } + internal static FixedFieldMap handlers = new() + { + { "type", (o,v) => {o.Type = v.GetString(); } }, + { "instructions", (o,v) => {o.Instructions = v.GetString(); } }, + }; + public override void Write(Utf8JsonWriter writer) { + writer.WriteStartObject(); + writer.WriteString("type", Type); + writer.WriteString("instructions", Instructions); + writer.WriteEndObject(); + } +} + + + + diff --git a/src/lib/OpenAI/OpenAIPluginManifest.cs b/src/lib/OpenAI/OpenAIPluginManifest.cs new file mode 100644 index 0000000..9f38944 --- /dev/null +++ b/src/lib/OpenAI/OpenAIPluginManifest.cs @@ -0,0 +1,66 @@ + +using System.Text.Json; + +namespace Microsoft.OpenApi.ApiManifest.OpenAI; + +public class OpenAIPluginManifest +{ + public string? SchemaVersion { get; set; } + public string? NameForHuman { get; set; } + public string? NameForModel { get; set; } + public string? DescriptionForHuman { get; set; } + public string? DescriptionForModel { get; set; } + public BaseManifestAuth? Auth { get; set; } + public Api? Api { get; set; } + public string? LogoUrl { get; set; } + public string? ContactEmail { get; set; } + public string? LegalInfoUrl { get; set; } + + + public static OpenAIPluginManifest Load(JsonElement value) + { + var manifest = new OpenAIPluginManifest(); + ParsingHelpers.ParseMap(value, manifest, handlers); + return manifest; + } + + // Create handlers FixedFieldMap for OpenAIPluginManifest + private static FixedFieldMap handlers = new() + { + { "schema_version", (o,v) => {o.SchemaVersion = v.GetString(); } }, + { "name_for_human", (o,v) => {o.NameForHuman = v.GetString(); } }, + { "name_for_model", (o,v) => {o.NameForModel = v.GetString(); } }, + { "description_for_human", (o,v) => {o.DescriptionForHuman = v.GetString(); } }, + { "description_for_model", (o,v) => {o.DescriptionForModel = v.GetString(); } }, + { "auth", (o,v) => {o.Auth = BaseManifestAuth.Load(v); } }, + { "api", (o,v) => {o.Api = Api.Load(v); } }, + { "logo_url", (o,v) => {o.LogoUrl = v.GetString(); } }, + { "contact_email", (o,v) => {o.ContactEmail = v.GetString(); } }, + { "legal_info_url", (o,v) => {o.LegalInfoUrl = v.GetString(); } }, + }; + + //Write method + public void Write(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + writer.WriteString("schema_version", SchemaVersion); + writer.WriteString("name_for_human", NameForHuman); + writer.WriteString("name_for_model", NameForModel); + writer.WriteString("description_for_human", DescriptionForHuman); + writer.WriteString("description_for_model", DescriptionForModel); + if (Auth != null) { + writer.WritePropertyName("auth"); + Auth.Write(writer); + } + if (Api != null) { + writer.WritePropertyName("api"); + Api?.Write(writer); + } + if (LogoUrl != null)writer.WriteString("logo_url", LogoUrl); + if (ContactEmail != null) writer.WriteString("contact_email", ContactEmail); + if (LegalInfoUrl != null) writer.WriteString("legal_info_url", LegalInfoUrl); + writer.WriteEndObject(); + } +} + + diff --git a/src/tests/PluginTests.cs b/src/tests/PluginTests.cs new file mode 100644 index 0000000..9bfe214 --- /dev/null +++ b/src/tests/PluginTests.cs @@ -0,0 +1,151 @@ +// Write tests for OpenAIPluginManifest + +using System.Text.Json; +using Microsoft.OpenApi.ApiManifest.OpenAI; + +namespace Tests.OpenAI +{ + public class OpenAIPluginManifestTests + { + [Fact] + public void LoadOpenAIPluginManifest() + { + var json = @"{ + ""schema_version"": ""1.0.0"", + ""name_for_human"": ""OpenAI GPT-3"", + ""name_for_model"": ""openai-gpt3"", + ""description_for_human"": ""OpenAI GPT-3 is a language model that generates text based on prompts."" , + ""description_for_model"": ""OpenAI GPT-3 is a language model that generates text based on prompts."", + ""auth"": { + ""type"": ""none"" + }, + ""api"": { + ""type"": ""openapi"", + ""url"": ""https://api.openai.com/v1"" + }, + ""logo_url"": ""https://avatars.githubusercontent.com/foo"", + ""contact_email"": ""joe@demo.com"" + }"; + + var doc = JsonDocument.Parse(json); + var manifest = OpenAIPluginManifest.Load(doc.RootElement); + + Assert.Equal("1.0.0", manifest.SchemaVersion); + Assert.Equal("OpenAI GPT-3", manifest.NameForHuman); + Assert.Equal("openai-gpt3", manifest.NameForModel); + Assert.Equal("OpenAI GPT-3 is a language model that generates text based on prompts.", manifest.DescriptionForHuman); + Assert.Equal("OpenAI GPT-3 is a language model that generates text based on prompts.", manifest.DescriptionForModel); + Assert.Equal("none", manifest.Auth?.Type); + Assert.Equal("openapi", manifest.Api?.Type); + Assert.Equal("https://api.openai.com/v1", manifest.Api?.Url); + Assert.Equal("https://avatars.githubusercontent.com/foo", manifest.LogoUrl); + Assert.Equal("joe@demo.com", manifest.ContactEmail); + } + + // Create minimal OpenAIPluginManifest + [Fact] + public void WriteOpenAIPluginManifest() + { + var manifest = new OpenAIPluginManifest + { + SchemaVersion = "1.0.0", + NameForHuman = "OpenAI GPT-3", + NameForModel = "openai-gpt3", + DescriptionForHuman = "OpenAI GPT-3 is a language model that generates text based on prompts.", + DescriptionForModel = "OpenAI GPT-3 is a language model that generates text based on prompts.", + Auth = new ManifestNoAuth(), + Api = new Api + { + Type = "openapi", + Url = "https://api.openai.com/v1", + IsUserAuthenticated = false + }, + LogoUrl = "https://avatars.githubusercontent.com/bar", + ContactEmail = "joe@test.com" + }; + + // serialize using the Write method + var stream = new MemoryStream(); + var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }); + manifest.Write(writer); + writer.Flush(); + stream.Position = 0; + var reader = new StreamReader(stream); + var json = reader.ReadToEnd(); + + Assert.Equal(@"{ + ""schema_version"": ""1.0.0"", + ""name_for_human"": ""OpenAI GPT-3"", + ""name_for_model"": ""openai-gpt3"", + ""description_for_human"": ""OpenAI GPT-3 is a language model that generates text based on prompts."", + ""description_for_model"": ""OpenAI GPT-3 is a language model that generates text based on prompts."", + ""auth"": { + ""type"": ""none"" + }, + ""api"": { + ""type"": ""openapi"", + ""url"": ""https://api.openai.com/v1"", + ""is_user_authenticated"": false + }, + ""logo_url"": ""https://avatars.githubusercontent.com/bar"", + ""contact_email"": ""joe@test.com"" +}", json, ignoreLineEndingDifferences: true,ignoreWhiteSpaceDifferences: true); + } + + [Fact] + public void WriteOAuthTest() + { + var manifest = new OpenAIPluginManifest + { + SchemaVersion = "1.0.0", + NameForHuman = "TestOAuth", + NameForModel = "TestOAuthModel", + DescriptionForHuman = "SomeHumanDescription", + DescriptionForModel = "SomeModelDescription", + Auth = new ManifestOAuthAuth + { + AuthorizationUrl = "https://api.openai.com/oauth/authorize", + }, + Api = new Api + { + Type = "openapi", + Url = "https://api.openai.com/v1", + IsUserAuthenticated = false + }, + LogoUrl = "https://avatars.githubusercontent.com/bar", + ContactEmail = "joe@test.com" + }; + + // serialize using the Write method + var stream = new MemoryStream(); + var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }); + manifest.Write(writer); + writer.Flush(); + stream.Position = 0; + var reader = new StreamReader(stream); + var json = reader.ReadToEnd(); + + Assert.Equal(@"{ + ""schema_version"": ""1.0.0"", + ""name_for_human"": ""TestOAuth"", + ""name_for_model"": ""TestOAuthModel"", + ""description_for_human"": ""SomeHumanDescription"", + ""description_for_model"": ""SomeModelDescription"", + ""auth"": { + ""type"": ""oauth"", + ""authorization_url"": ""https://api.openai.com/oauth/authorize"" + }, + ""api"": { + ""type"": ""openapi"", + ""url"": ""https://api.openai.com/v1"", + ""is_user_authenticated"": false + }, + ""logo_url"": ""https://avatars.githubusercontent.com/bar"", + ""contact_email"": ""joe@test.com"" +}", json, ignoreLineEndingDifferences: true,ignoreWhiteSpaceDifferences: true); + } + + } + + +} \ No newline at end of file