Skip to content

Commit

Permalink
perf(spec-parser): sme auth code flow, update teams ai filter logic (#…
Browse files Browse the repository at this point in the history
…11058)

* perf(spec-parser): sme auth code flow, update teams ai filter logic

* perf: remove unused code

---------

Co-authored-by: turenlong <rentu@microsoft.com>
  • Loading branch information
SLdragon and SLdragon authored Mar 13, 2024
1 parent 8b9cd6a commit e7482a0
Show file tree
Hide file tree
Showing 9 changed files with 617 additions and 103 deletions.
5 changes: 3 additions & 2 deletions packages/spec-parser/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export class ConstantString {
static readonly SwaggerNotSupported =
"Swagger 2.0 is not supported. Please convert to OpenAPI 3.0 manually before proceeding.";

static readonly MultipleAPIKeyNotSupported =
"Multiple API keys are not supported. Please make sure that all selected APIs use the same API key.";
static readonly MultipleAuthNotSupported =
"Multiple authentication methods are unsupported. Ensure all selected APIs use identical authentication.";

static readonly UnsupportedSchema = "Unsupported schema in %s %s: %s";

Expand All @@ -48,6 +48,7 @@ export class ConstantString {
static readonly TextBlockType = "TextBlock";
static readonly ContainerType = "Container";
static readonly RegistrationIdPostfix = "REGISTRATION_ID";
static readonly OAuthRegistrationIdPostFix = "OAUTH_REGISTRATION_ID";
static readonly ResponseCodeFor20X = [
"200",
"201",
Expand Down
8 changes: 4 additions & 4 deletions packages/spec-parser/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export enum ErrorType {
NoExtraAPICanBeAdded = "no-extra-api-can-be-added",
ResolveServerUrlFailed = "resolve-server-url-failed",
SwaggerNotSupported = "swagger-not-supported",
MultipleAPIKeyNotSupported = "multiple-api-key-not-supported",
MultipleAuthNotSupported = "multiple-auth-not-supported",

ListFailed = "list-failed",
listSupportedAPIInfoFailed = "list-supported-api-info-failed",
Expand Down Expand Up @@ -198,12 +198,12 @@ export interface ParseOptions {
allowAPIKeyAuth?: boolean;

/**
* If true, the parser will allow multiple parameters in the spec file.
* If true, the parser will allow multiple parameters in the spec file. Teams AI project would ignore this parameters and always true
*/
allowMultipleParameters?: boolean;

/**
* If true, the parser will allow OAuth2 authentication in the spec file.
* If true, the parser will allow OAuth2 authentication in the spec file. Currently only support OAuth2 with auth code flow.
*/
allowOauth2?: boolean;

Expand Down Expand Up @@ -242,7 +242,7 @@ export interface ListAPIResult {
auth?: OpenAPIV3.SecuritySchemeObject;
}

export interface AuthSchema {
export interface AuthInfo {
authSchema: OpenAPIV3.SecuritySchemeObject;
name: string;
}
21 changes: 13 additions & 8 deletions packages/spec-parser/src/manifestUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { OpenAPIV3 } from "openapi-types";
import fs from "fs-extra";
import path from "path";
import { ErrorType, ParseOptions, ProjectType, WarningResult } from "./interfaces";
import { AuthInfo, ErrorType, ParseOptions, ProjectType, WarningResult } from "./interfaces";
import { Utils } from "./utils";
import { SpecParserError } from "./specParserError";
import { ConstantString } from "./constants";
Expand Down Expand Up @@ -200,7 +200,7 @@ export class ManifestUpdater {
spec: OpenAPIV3.Document,
options: ParseOptions,
adaptiveCardFolder?: string,
auth?: OpenAPIV3.SecuritySchemeObject
authInfo?: AuthInfo
): Promise<[TeamsAppManifest, WarningResult[]]> {
try {
const originalManifest: TeamsAppManifest = await fs.readJSON(manifestPath);
Expand All @@ -224,23 +224,28 @@ export class ManifestUpdater {
commands: commands,
};

if (auth) {
if (authInfo) {
let auth = authInfo.authSchema;
if (Utils.isAPIKeyAuth(auth)) {
auth = auth as OpenAPIV3.ApiKeySecurityScheme;
const safeApiSecretRegistrationId = Utils.getSafeRegistrationIdEnvName(
`${auth.name}_${ConstantString.RegistrationIdPostfix}`
`${authInfo.name}_${ConstantString.RegistrationIdPostfix}`
);
(composeExtension as any).authorization = {
authType: "apiSecretServiceAuth",
apiSecretServiceAuthConfiguration: {
apiSecretRegistrationId: `\${{${safeApiSecretRegistrationId}}}`,
},
};
} else if (Utils.isBearerTokenAuth(auth)) {
} else if (Utils.isOAuthWithAuthCodeFlow(auth)) {
const safeOAuth2RegistrationId = Utils.getSafeRegistrationIdEnvName(
`${authInfo.name}_${ConstantString.OAuthRegistrationIdPostFix}`
);

(composeExtension as any).authorization = {
authType: "microsoftEntra",
microsoftEntraConfiguration: {
supportsSingleSignOn: true,
authType: "oAuth2.0",
oAuthConfiguration: {
oauthConfigurationId: `\${{${safeOAuth2RegistrationId}}}`,
},
};

Expand Down
21 changes: 11 additions & 10 deletions packages/spec-parser/src/specParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import fs from "fs-extra";
import path from "path";
import {
APIInfo,
AuthInfo,
ErrorType,
GenerateResult,
ListAPIResult,
Expand Down Expand Up @@ -282,8 +283,8 @@ export class SpecParser {
const newUnResolvedSpec = newSpecs[0];
const newSpec = newSpecs[1];

const AuthSet: Set<OpenAPIV3.SecuritySchemeObject> = new Set();
let hasMultipleAPIKeyAuth = false;
const authSet: Set<AuthInfo> = new Set();
let hasMultipleAuth = false;

for (const url in newSpec.paths) {
for (const method in newSpec.paths[url]) {
Expand All @@ -292,19 +293,19 @@ export class SpecParser {
const authArray = Utils.getAuthArray(operation.security, newSpec);

if (authArray && authArray.length > 0) {
AuthSet.add(authArray[0][0].authSchema);
if (AuthSet.size > 1) {
hasMultipleAPIKeyAuth = true;
authSet.add(authArray[0][0]);
if (authSet.size > 1) {
hasMultipleAuth = true;
break;
}
}
}
}

if (hasMultipleAPIKeyAuth) {
if (hasMultipleAuth && this.options.projectType !== ProjectType.TeamsAi) {
throw new SpecParserError(
ConstantString.MultipleAPIKeyNotSupported,
ErrorType.MultipleAPIKeyNotSupported
ConstantString.MultipleAuthNotSupported,
ErrorType.MultipleAuthNotSupported
);
}

Expand Down Expand Up @@ -349,14 +350,14 @@ export class SpecParser {
throw new SpecParserError(ConstantString.CancelledMessage, ErrorType.Cancelled);
}

const auth = Array.from(AuthSet)[0];
const authInfo = Array.from(authSet)[0];
const [updatedManifest, warnings] = await ManifestUpdater.updateManifest(
manifestPath,
outputSpecPath,
newSpec,
this.options,
adaptiveCardFolder,
auth
authInfo
);

await fs.outputJSON(manifestPath, updatedManifest, { spaces: 2 });
Expand Down
74 changes: 45 additions & 29 deletions packages/spec-parser/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { OpenAPIV3 } from "openapi-types";
import SwaggerParser from "@apidevtools/swagger-parser";
import { ConstantString } from "./constants";
import {
AuthSchema,
AuthInfo,
CheckParamResult,
ErrorResult,
ErrorType,
Expand Down Expand Up @@ -155,6 +155,12 @@ export class Utils {
return paramResult;
}

static containMultipleMediaTypes(
bodyObject: OpenAPIV3.RequestBodyObject | OpenAPIV3.ResponseObject
): boolean {
return Object.keys(bodyObject?.content || {}).length > 1;
}

/**
* Checks if the given API is supported.
* @param {string} method - The HTTP method of the API.
Expand All @@ -180,9 +186,17 @@ export class Utils {
if (pathObj) {
if (options.allowMethods?.includes(method) && pathObj[method]) {
const securities = pathObj[method].security;
const authArray = Utils.getAuthArray(securities, spec);
if (!Utils.isSupportedAuth(authArray, options)) {
return false;

const isTeamsAi = options.projectType === ProjectType.TeamsAi;
const isCopilot = options.projectType === ProjectType.Copilot;

// Teams AI project doesn't care about auth, it will use authProvider for user to implement
if (!isTeamsAi) {
const authArray = Utils.getAuthArray(securities, spec);

if (!Utils.isSupportedAuth(authArray, options)) {
return false;
}
}

const operationObject = pathObj[method] as OpenAPIV3.OperationObject;
Expand All @@ -194,24 +208,27 @@ export class Utils {
const requestBody = operationObject.requestBody as OpenAPIV3.RequestBodyObject;
const requestJsonBody = requestBody?.content["application/json"];

const mediaTypesCount = Object.keys(requestBody?.content || {}).length;
if (mediaTypesCount > 1) {
if (!isTeamsAi && Utils.containMultipleMediaTypes(requestBody)) {
return false;
}

const responseJson = Utils.getResponseJson(operationObject);
const responseJson = Utils.getResponseJson(operationObject, isTeamsAi);

if (Object.keys(responseJson).length === 0) {
return false;
}

// Teams AI project doesn't care about request parameters/body
if (isTeamsAi) {
return true;
}

let requestBodyParamResult = {
requiredNum: 0,
optionalNum: 0,
isValid: true,
};

const isCopilot = options.projectType === ProjectType.Copilot;

if (requestJsonBody) {
const requestBodySchema = requestJsonBody.schema as OpenAPIV3.SchemaObject;

Expand Down Expand Up @@ -267,7 +284,7 @@ export class Utils {
return false;
}

static isSupportedAuth(authSchemaArray: AuthSchema[][], options: ParseOptions): boolean {
static isSupportedAuth(authSchemaArray: AuthInfo[][], options: ParseOptions): boolean {
if (authSchemaArray.length === 0) {
return true;
}
Expand All @@ -289,14 +306,14 @@ export class Utils {
} else if (
!options.allowAPIKeyAuth &&
options.allowOauth2 &&
Utils.isBearerTokenAuth(auths[0].authSchema)
Utils.isOAuthWithAuthCodeFlow(auths[0].authSchema)
) {
return true;
} else if (
options.allowAPIKeyAuth &&
options.allowOauth2 &&
(Utils.isAPIKeyAuth(auths[0].authSchema) ||
Utils.isBearerTokenAuth(auths[0].authSchema))
Utils.isOAuthWithAuthCodeFlow(auths[0].authSchema))
) {
return true;
}
Expand All @@ -311,25 +328,25 @@ export class Utils {
return authSchema.type === "apiKey";
}

static isBearerTokenAuth(authSchema: OpenAPIV3.SecuritySchemeObject): boolean {
return (
authSchema.type === "oauth2" ||
authSchema.type === "openIdConnect" ||
(authSchema.type === "http" && authSchema.scheme === "bearer")
);
static isOAuthWithAuthCodeFlow(authSchema: OpenAPIV3.SecuritySchemeObject): boolean {
if (authSchema.type === "oauth2" && authSchema.flows && authSchema.flows.authorizationCode) {
return true;
}

return false;
}

static getAuthArray(
securities: OpenAPIV3.SecurityRequirementObject[] | undefined,
spec: OpenAPIV3.Document
): AuthSchema[][] {
const result: AuthSchema[][] = [];
): AuthInfo[][] {
const result: AuthInfo[][] = [];
const securitySchemas = spec.components?.securitySchemes;
if (securities && securitySchemas) {
for (let i = 0; i < securities.length; i++) {
const security = securities[i];

const authArray: AuthSchema[] = [];
const authArray: AuthInfo[] = [];
for (const name in security) {
const auth = securitySchemas[name] as OpenAPIV3.SecuritySchemeObject;
authArray.push({
Expand All @@ -354,21 +371,21 @@ export class Utils {
}

static getResponseJson(
operationObject: OpenAPIV3.OperationObject | undefined
operationObject: OpenAPIV3.OperationObject | undefined,
isTeamsAiProject = false
): OpenAPIV3.MediaTypeObject {
let json: OpenAPIV3.MediaTypeObject = {};

for (const code of ConstantString.ResponseCodeFor20X) {
const responseObject = operationObject?.responses?.[code] as OpenAPIV3.ResponseObject;

const mediaTypesCount = Object.keys(responseObject?.content || {}).length;
if (mediaTypesCount > 1) {
return {};
}

if (responseObject?.content?.["application/json"]) {
json = responseObject.content["application/json"];
break;
if (!isTeamsAiProject && Utils.containMultipleMediaTypes(responseObject)) {
json = {};
} else {
break;
}
}
}

Expand Down Expand Up @@ -674,7 +691,6 @@ export class Utils {
for (const path in paths) {
const methods = paths[path];
for (const method in methods) {
// For developer preview, only support GET operation with only 1 parameter without auth
if (Utils.isSupportedApi(method, path, spec, options)) {
const operationObject = (methods as any)[method] as OpenAPIV3.OperationObject;
result[`${method.toUpperCase()} ${path}`] = operationObject;
Expand Down
36 changes: 18 additions & 18 deletions packages/spec-parser/test/adaptiveCardGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,27 +351,27 @@ describe("adaptiveCardGenerator", () => {
expect(actual).to.deep.equal(expected);
expect(jsonPath).to.equal("$");
});
});

it("should generate a card if schema is empty", () => {
const schema = {};
const expected = {
type: "AdaptiveCard",
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
version: "1.5",
body: [
{
type: "TextBlock",
text: "success",
wrap: true,
},
],
};
it("should generate a card if schema is empty", () => {
const schema = {};
const expected = {
type: "AdaptiveCard",
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
version: "1.5",
body: [
{
type: "TextBlock",
text: "success",
wrap: true,
},
],
};

const [actual, jsonPath] = AdaptiveCardGenerator.generateAdaptiveCard(schema);
const [actual, jsonPath] = AdaptiveCardGenerator.generateAdaptiveCard(schema);

expect(actual).to.deep.equal(expected);
expect(jsonPath).to.equal("$");
expect(actual).to.deep.equal(expected);
expect(jsonPath).to.equal("$");
});
});

describe("generateCardFromResponse", () => {
Expand Down
Loading

0 comments on commit e7482a0

Please sign in to comment.