From 9355002a7089039062a7c9465c641df3cb313bac Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 2 Jul 2024 09:56:32 -0700 Subject: [PATCH 1/4] Set OpenAPI3 Extensions --- packages/http/src/auth.ts | 18 +++++++++--------- packages/http/src/decorators.ts | 8 ++++++-- packages/http/src/types.ts | 3 +++ packages/openapi3/src/openapi.ts | 8 ++++++++ packages/openapi3/test/security.test.ts | 21 +++++++++++++++++++++ 5 files changed, 47 insertions(+), 11 deletions(-) diff --git a/packages/http/src/auth.ts b/packages/http/src/auth.ts index 4220828ba5..d9a8a295a4 100644 --- a/packages/http/src/auth.ts +++ b/packages/http/src/auth.ts @@ -10,7 +10,6 @@ import { HttpService, HttpServiceAuthentication, OAuth2Flow, - OAuth2Scope, Oauth2Auth, } from "./types.js"; @@ -141,13 +140,12 @@ function mergeOAuthScopes( }; } -function setOauth2Scopes( - scheme: Oauth2Auth, - scopes: OAuth2Scope[] -): Oauth2Auth { +function ignoreScopes( + scheme: Omit, "model"> +): Omit, "model"> { const flows: Flows = deepClone(scheme.flows); flows.forEach((flow) => { - flow.scopes = scopes; + flow.scopes = []; }); return { ...scheme, @@ -156,8 +154,10 @@ function setOauth2Scopes( } function authsAreEqual(scheme1: HttpAuth, scheme2: HttpAuth): boolean { - if (scheme1.type === "oauth2" && scheme2.type === "oauth2") { - return deepEquals(setOauth2Scopes(scheme1, []), setOauth2Scopes(scheme2, [])); + const { model: _model1, ...withoutModel1 } = scheme1; + const { model: _model2, ...withoutModel2 } = scheme2; + if (withoutModel1.type === "oauth2" && withoutModel2.type === "oauth2") { + return deepEquals(ignoreScopes(withoutModel1), ignoreScopes(withoutModel2)); } - return deepEquals(scheme1, scheme2); + return deepEquals(withoutModel1, withoutModel2); } diff --git a/packages/http/src/decorators.ts b/packages/http/src/decorators.ts index f0dec1eefc..51a8c4e9ad 100644 --- a/packages/http/src/decorators.ts +++ b/packages/http/src/decorators.ts @@ -584,7 +584,10 @@ function extractHttpAuthentication( return [result, diagnostics]; } const description = getDoc(program, modelType); - const auth = result.type === "oauth2" ? extractOAuth2Auth(result) : result; + const auth = + result.type === "oauth2" + ? extractOAuth2Auth(modelType, result) + : { ...result, model: modelType }; return [ { ...auth, @@ -595,7 +598,7 @@ function extractHttpAuthentication( ]; } -function extractOAuth2Auth(data: any): HttpAuth { +function extractOAuth2Auth(modelType: Model, data: any): HttpAuth { // Validation of OAuth2Flow models in this function is minimal because the // type system already validates whether the model represents a flow // configuration. This code merely avoids runtime errors. @@ -608,6 +611,7 @@ function extractOAuth2Auth(data: any): HttpAuth { return { id: data.id, type: data.type, + model: modelType, flows: flows.map((flow: any) => { const scopes: Array = flow.scopes ? flow.scopes : defaultScopes; return { diff --git a/packages/http/src/types.ts b/packages/http/src/types.ts index 0b3ea86a0d..5732936799 100644 --- a/packages/http/src/types.ts +++ b/packages/http/src/types.ts @@ -55,6 +55,9 @@ export interface HttpAuthBase { * Optional description. */ description?: string; + + /** Model that defined the authentication */ + readonly model: Model; } /** diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index 01538af7cf..179ef0d89f 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -1816,6 +1816,14 @@ function createOAPIEmitter( } function getOpenAPI3Scheme(auth: HttpAuth): OpenAPI3SecurityScheme | undefined { + const scheme = getOpenAPI3SchemeInternal(auth); + + if (scheme) { + attachExtensions(program, auth.model, scheme); + } + return scheme; + } + function getOpenAPI3SchemeInternal(auth: HttpAuth): OpenAPI3SecurityScheme | undefined { switch (auth.type) { case "http": return { type: "http", scheme: auth.scheme, description: auth.description }; diff --git a/packages/openapi3/test/security.test.ts b/packages/openapi3/test/security.test.ts index 52143fccca..25010eaf68 100644 --- a/packages/openapi3/test/security.test.ts +++ b/packages/openapi3/test/security.test.ts @@ -128,6 +128,27 @@ describe("openapi3: security", () => { deepStrictEqual(res.security, [{ MyAuth: [] }]); }); + it("can specify custom auth name with extensions", async () => { + const res = await openApiFor( + ` + @service({title: "My service"}) + @useAuth(MyAuth) + @test namespace Foo { + @extension("x-foo", "bar") + model MyAuth is BasicAuth; + } + ` + ); + deepStrictEqual(res.components.securitySchemes, { + MyAuth: { + type: "http", + scheme: "basic", + "x-foo": "bar", + }, + }); + deepStrictEqual(res.security, [{ MyAuth: [] }]); + }); + it("can use multiple auth", async () => { const res = await openApiFor( ` From 57369dfe07438501f3952c44d8d6dd79855ad3e4 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 2 Jul 2024 10:05:13 -0700 Subject: [PATCH 2/4] Create set-openapi3-extension-security-schemes-2024-6-2-17-4-20.md --- ...penapi3-extension-security-schemes-2024-6-2-17-4-20.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .chronus/changes/set-openapi3-extension-security-schemes-2024-6-2-17-4-20.md diff --git a/.chronus/changes/set-openapi3-extension-security-schemes-2024-6-2-17-4-20.md b/.chronus/changes/set-openapi3-extension-security-schemes-2024-6-2-17-4-20.md new file mode 100644 index 0000000000..812dec5b14 --- /dev/null +++ b/.chronus/changes/set-openapi3-extension-security-schemes-2024-6-2-17-4-20.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: feature +packages: + - "@typespec/openapi3" +--- + +Apply openapi3 extension on Security schemes From c009db1a1f162db7e01407b9bdea364d8bce1328 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 2 Jul 2024 10:07:56 -0700 Subject: [PATCH 3/4] Create set-openapi3-extension-security-schemes-2024-6-2-17-7-34.md --- ...penapi3-extension-security-schemes-2024-6-2-17-7-34.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .chronus/changes/set-openapi3-extension-security-schemes-2024-6-2-17-7-34.md diff --git a/.chronus/changes/set-openapi3-extension-security-schemes-2024-6-2-17-7-34.md b/.chronus/changes/set-openapi3-extension-security-schemes-2024-6-2-17-7-34.md new file mode 100644 index 0000000000..b75808a1a4 --- /dev/null +++ b/.chronus/changes/set-openapi3-extension-security-schemes-2024-6-2-17-7-34.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: feature +packages: + - "@typespec/http" +--- + +Expose `model` property on `HttpAuth` to retrieve original type used to define security scheme From 6bf62ac292a41efcfa616b0c3ba69bebb4a0dafd Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 2 Jul 2024 11:05:17 -0700 Subject: [PATCH 4/4] fix tests --- packages/http/test/http-decorators.test.ts | 121 +++++++++++++++++---- 1 file changed, 97 insertions(+), 24 deletions(-) diff --git a/packages/http/test/http-decorators.test.ts b/packages/http/test/http-decorators.test.ts index cc4a655b3a..3bd5329df0 100644 --- a/packages/http/test/http-decorators.test.ts +++ b/packages/http/test/http-decorators.test.ts @@ -5,7 +5,7 @@ import { expectDiagnostics, } from "@typespec/compiler/testing"; import { deepStrictEqual, ok, strictEqual } from "assert"; -import { beforeEach, describe, it } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; import { getAuthentication, getHeaderFieldName, @@ -726,7 +726,7 @@ describe("http: decorators", () => { @test namespace Foo {} `)) as { Foo: Namespace }; - deepStrictEqual(getAuthentication(runner.program, Foo), { + expect(getAuthentication(runner.program, Foo)).toEqual({ options: [ { schemes: [ @@ -734,6 +734,7 @@ describe("http: decorators", () => { id: "BasicAuth", type: "http", scheme: "basic", + model: expect.objectContaining({ kind: "Model" }), }, ], }, @@ -749,11 +750,17 @@ describe("http: decorators", () => { @test namespace Foo {} `)) as { Foo: Namespace }; - deepStrictEqual(getAuthentication(runner.program, Foo), { + expect(getAuthentication(runner.program, Foo)).toEqual({ options: [ { schemes: [ - { id: "MyAuth", description: "My custom basic auth", type: "http", scheme: "basic" }, + { + id: "MyAuth", + description: "My custom basic auth", + type: "http", + scheme: "basic", + model: expect.objectContaining({ kind: "Model" }), + }, ], }, ], @@ -766,7 +773,7 @@ describe("http: decorators", () => { @test namespace Foo {} `)) as { Foo: Namespace }; - deepStrictEqual(getAuthentication(runner.program, Foo), { + expect(getAuthentication(runner.program, Foo)).toEqual({ options: [ { schemes: [ @@ -774,6 +781,7 @@ describe("http: decorators", () => { id: "BearerAuth", type: "http", scheme: "bearer", + model: expect.objectContaining({ kind: "Model" }), }, ], }, @@ -787,7 +795,7 @@ describe("http: decorators", () => { @test namespace Foo {} `)) as { Foo: Namespace }; - deepStrictEqual(getAuthentication(runner.program, Foo), { + expect(getAuthentication(runner.program, Foo)).toEqual({ options: [ { schemes: [ @@ -796,6 +804,7 @@ describe("http: decorators", () => { type: "apiKey", in: "header", name: "x-my-header", + model: expect.objectContaining({ kind: "Model" }), }, ], }, @@ -815,7 +824,7 @@ describe("http: decorators", () => { @test namespace Foo {} `)) as { Foo: Namespace }; - deepStrictEqual(getAuthentication(runner.program, Foo), { + expect(getAuthentication(runner.program, Foo)).toEqual({ options: [ { schemes: [ @@ -830,6 +839,7 @@ describe("http: decorators", () => { scopes: [{ value: "read" }, { value: "write" }], }, ], + model: expect.objectContaining({ kind: "Model" }), }, ], }, @@ -849,7 +859,7 @@ describe("http: decorators", () => { @test namespace Foo {} `)) as { Foo: Namespace }; - deepStrictEqual(getAuthentication(runner.program, Foo), { + expect(getAuthentication(runner.program, Foo)).toEqual({ options: [ { schemes: [ @@ -864,6 +874,7 @@ describe("http: decorators", () => { scopes: [{ value: "read" }, { value: "write" }], }, ], + model: expect.objectContaining({ kind: "Model" }), }, ], }, @@ -877,13 +888,14 @@ describe("http: decorators", () => { @test namespace Foo {} `)) as { Foo: Namespace }; - deepStrictEqual(getAuthentication(runner.program, Foo), { + expect(getAuthentication(runner.program, Foo)).toEqual({ options: [ { schemes: [ { id: "NoAuth", type: "noAuth", + model: expect.objectContaining({ kind: "Model" }), }, ], }, @@ -897,13 +909,27 @@ describe("http: decorators", () => { @test namespace Foo {} `)) as { Foo: Namespace }; - deepStrictEqual(getAuthentication(runner.program, Foo), { + expect(getAuthentication(runner.program, Foo)).toEqual({ options: [ { - schemes: [{ id: "BasicAuth", type: "http", scheme: "basic" }], + schemes: [ + { + id: "BasicAuth", + type: "http", + scheme: "basic", + model: expect.objectContaining({ kind: "Model" }), + }, + ], }, { - schemes: [{ id: "BearerAuth", type: "http", scheme: "bearer" }], + schemes: [ + { + id: "BearerAuth", + type: "http", + scheme: "bearer", + model: expect.objectContaining({ kind: "Model" }), + }, + ], }, ], }); @@ -915,12 +941,22 @@ describe("http: decorators", () => { @test namespace Foo {} `)) as { Foo: Namespace }; - deepStrictEqual(getAuthentication(runner.program, Foo), { + expect(getAuthentication(runner.program, Foo)).toEqual({ options: [ { schemes: [ - { id: "BasicAuth", type: "http", scheme: "basic" }, - { id: "BearerAuth", type: "http", scheme: "bearer" }, + { + id: "BasicAuth", + type: "http", + scheme: "basic", + model: expect.objectContaining({ kind: "Model" }), + }, + { + id: "BearerAuth", + type: "http", + scheme: "bearer", + model: expect.objectContaining({ kind: "Model" }), + }, ], }, ], @@ -933,10 +969,17 @@ describe("http: decorators", () => { @test namespace Foo {} `)) as { Foo: Namespace }; - deepStrictEqual(getAuthentication(runner.program, Foo), { + expect(getAuthentication(runner.program, Foo)).toEqual({ options: [ { - schemes: [{ id: "BearerAuth", type: "http", scheme: "bearer" }], + schemes: [ + { + id: "BearerAuth", + type: "http", + scheme: "bearer", + model: expect.objectContaining({ kind: "Model" }), + }, + ], }, { schemes: [ @@ -945,8 +988,14 @@ describe("http: decorators", () => { type: "apiKey", in: "header", name: "x-my-header", + model: expect.objectContaining({ kind: "Model" }), + }, + { + id: "BasicAuth", + type: "http", + scheme: "basic", + model: expect.objectContaining({ kind: "Model" }), }, - { id: "BasicAuth", type: "http", scheme: "basic" }, ], }, ], @@ -963,13 +1012,27 @@ describe("http: decorators", () => { } `)) as { Foo: Namespace }; - deepStrictEqual(getAuthentication(runner.program, Foo.interfaces.get("Bar")!), { + expect(getAuthentication(runner.program, Foo.interfaces.get("Bar")!)).toEqual({ options: [ { - schemes: [{ id: "BasicAuth", type: "http", scheme: "basic" }], + schemes: [ + { + id: "BasicAuth", + type: "http", + scheme: "basic", + model: expect.objectContaining({ kind: "Model" }), + }, + ], }, { - schemes: [{ id: "BearerAuth", type: "http", scheme: "bearer" }], + schemes: [ + { + id: "BearerAuth", + type: "http", + scheme: "bearer", + model: expect.objectContaining({ kind: "Model" }), + }, + ], }, ], }); @@ -985,12 +1048,22 @@ describe("http: decorators", () => { } `)) as { Foo: Namespace }; - deepStrictEqual(getAuthentication(runner.program, Foo.operations.get("bar")!), { + expect(getAuthentication(runner.program, Foo.operations.get("bar")!)).toEqual({ options: [ { schemes: [ - { id: "BasicAuth", type: "http", scheme: "basic" }, - { id: "BearerAuth", type: "http", scheme: "bearer" }, + { + id: "BasicAuth", + type: "http", + scheme: "basic", + model: expect.objectContaining({ kind: "Model" }), + }, + { + id: "BearerAuth", + type: "http", + scheme: "bearer", + model: expect.objectContaining({ kind: "Model" }), + }, ], }, ],