Skip to content

Commit

Permalink
perf(type-b): update add new api logic to support oauth (#11515)
Browse files Browse the repository at this point in the history
* perf(type-b): update add new api logic

* perf: update message

* perf: move common logic to configManager

* perf: remove unexpected change

* fix: reference issue

---------

Co-authored-by: rentu <rentu@microsoft.com>
  • Loading branch information
SLdragon and SLdragon authored May 6, 2024
1 parent e1b11d5 commit 0d4f61e
Show file tree
Hide file tree
Showing 5 changed files with 558 additions and 86 deletions.
3 changes: 2 additions & 1 deletion packages/fx-core/resource/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,8 @@
"core.summary.actionSucceeded": "%s was executed successfully.",
"core.summary.createdEnvFile": "Environment file was created at",
"core.copilot.addAPI.success": "%s have(has) been successfully added to %s",
"core.copilot.addAPI.InjectAPIKeyActionFailed": "Inject API key action to teamsapp.yaml file failed, please make sure that file contains teamsApp/create action in provision section.",
"core.copilot.addAPI.InjectAPIKeyActionFailed": "Inject API key action to teamsapp.yaml file unsuccessful, make sure the file contains teamsApp/create action in provision section.",
"core.copilot.addAPI.InjectOAuthActionFailed": "Inject OAuth action to teamsapp.yaml file unsuccessful, make sure the file contains teamsApp/create action in provision section.",
"ui.select.LoadingOptionsPlaceholder": "Loading options ...",
"ui.select.LoadingDefaultPlaceholder": "Loading default value ...",
"error.aad.manifest.NameIsMissing": "name is missing\n",
Expand Down
143 changes: 143 additions & 0 deletions packages/fx-core/src/component/configManager/actionInjector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { Utils } from "@microsoft/m365-spec-parser";
import fs from "fs-extra";
import { parseDocument } from "yaml";
import { InjectAPIKeyActionFailedError, InjectOAuthActionFailedError } from "../../error/common";

export class ActionInjector {
static hasActionWithName(provisionNode: any, action: string, name: string): any {
const hasAuthAction = provisionNode.items.some(
(item: any) => item.get("uses") === action && item.get("with")?.get("name") === name
);
return hasAuthAction;
}

static getTeamsAppIdEnvName(provisionNode: any): string | undefined {
for (const item of provisionNode.items) {
if (item.get("uses") === "teamsApp/create") {
return item.get("writeToEnvironmentFile")?.get("teamsAppId") as string;
}
}

return undefined;
}

static generateAuthAction(
actionName: string,
authName: string,
teamsAppIdEnvName: string,
specRelativePath: string,
envName: string,
flow?: string
): any {
const result: any = {
uses: actionName,
with: {
name: `${authName}`,
appId: `\${{${teamsAppIdEnvName}}}`,
apiSpecPath: specRelativePath,
},
};

if (flow) {
result.with.flow = flow;
result.writeToEnvironmentFile = {
configurationId: envName,
};
} else {
result.writeToEnvironmentFile = {
registrationId: envName,
};
}

return result;
}

static async injectCreateOAuthAction(
ymlPath: string,
authName: string,
specRelativePath: string
): Promise<void> {
const ymlContent = await fs.readFile(ymlPath, "utf-8");
const actionName = "oauth/register";

const document = parseDocument(ymlContent);
const provisionNode = document.get("provision") as any;
if (provisionNode) {
const hasOAuthAction = ActionInjector.hasActionWithName(provisionNode, actionName, authName);
if (!hasOAuthAction) {
provisionNode.items = provisionNode.items.filter(
(item: any) => item.get("uses") !== actionName && item.get("uses") !== "apiKey/register"
);
const teamsAppIdEnvName = ActionInjector.getTeamsAppIdEnvName(provisionNode);
if (teamsAppIdEnvName) {
const index: number = provisionNode.items.findIndex(
(item: any) => item.get("uses") === "teamsApp/create"
);
const envName = Utils.getSafeRegistrationIdEnvName(`${authName}_CONFIGURATION_ID`);
const flow = "authorizationCode";
const action = ActionInjector.generateAuthAction(
actionName,
authName,
teamsAppIdEnvName,
specRelativePath,
envName,
flow
);
provisionNode.items.splice(index + 1, 0, action);
} else {
throw new InjectOAuthActionFailedError();
}

await fs.writeFile(ymlPath, document.toString(), "utf8");
}
} else {
throw new InjectOAuthActionFailedError();
}
}

static async injectCreateAPIKeyAction(
ymlPath: string,
authName: string,
specRelativePath: string
): Promise<void> {
const ymlContent = await fs.readFile(ymlPath, "utf-8");
const actionName = "apiKey/register";

const document = parseDocument(ymlContent);
const provisionNode = document.get("provision") as any;

if (provisionNode) {
const hasApiKeyAction = ActionInjector.hasActionWithName(provisionNode, actionName, authName);

if (!hasApiKeyAction) {
provisionNode.items = provisionNode.items.filter(
(item: any) => item.get("uses") !== actionName && item.get("uses") !== "oauth/register"
);
const teamsAppIdEnvName = ActionInjector.getTeamsAppIdEnvName(provisionNode);
if (teamsAppIdEnvName) {
const index: number = provisionNode.items.findIndex(
(item: any) => item.get("uses") === "teamsApp/create"
);
const envName = Utils.getSafeRegistrationIdEnvName(`${authName}_REGISTRATION_ID`);
const action = ActionInjector.generateAuthAction(
actionName,
authName,
teamsAppIdEnvName,
specRelativePath,
envName
);
provisionNode.items.splice(index + 1, 0, action);
} else {
throw new InjectAPIKeyActionFailedError();
}

await fs.writeFile(ymlPath, document.toString(), "utf8");
}
} else {
throw new InjectAPIKeyActionFailedError();
}
}
}
127 changes: 42 additions & 85 deletions packages/fx-core/src/core/FxCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
err,
ok,
} from "@microsoft/teamsfx-api";
import { OpenAPIV3 } from "openapi-types";
import { DotenvParseOutput } from "dotenv";
import fs from "fs-extra";
import * as jsonschema from "jsonschema";
Expand Down Expand Up @@ -108,7 +109,6 @@ import { pathUtils } from "../component/utils/pathUtils";
import { settingsUtil } from "../component/utils/settingsUtil";
import {
FileNotFoundError,
InjectAPIKeyActionFailedError,
InputValidationError,
InvalidProjectError,
MissingRequiredInputError,
Expand Down Expand Up @@ -149,6 +149,7 @@ import { CoreHookContext, PreProvisionResForVS, VersionCheckRes } from "./types"
import { AppStudioResultFactory } from "../component/driver/teamsApp/results";
import { AppStudioError } from "../component/driver/teamsApp/errors";
import { copilotGptManifestUtils } from "../component/driver/teamsApp/utils/CopilotGptManifestUtils";
import { ActionInjector } from "../component/configManager/actionInjector";

export type CoreCallbackFunc = (name: string, err?: FxError, data?: any) => void | Promise<void>;

Expand Down Expand Up @@ -1240,61 +1241,6 @@ export class FxCore {
return await coordinator.publishInDeveloperPortal(context, inputs as InputsWithProjectPath);
}

async injectCreateAPIKeyAction(
ymlPath: string,
authName: string,
specRelativePath: string
): Promise<void> {
const ymlContent = await fs.readFile(ymlPath, "utf-8");

const document = parseDocument(ymlContent);
const provisionNode = document.get("provision") as any;

if (provisionNode) {
const hasApiKeyAction = provisionNode.items.some(
(item: any) =>
item.get("uses") === "apiKey/register" && item.get("with")?.get("name") === authName
);

if (!hasApiKeyAction) {
provisionNode.items = provisionNode.items.filter(
(item: any) => item.get("uses") !== "apiKey/register"
);
let added = false;
for (let i = 0; i < provisionNode.items.length; i++) {
const item = provisionNode.items[i];
if (item.get("uses") === "teamsApp/create") {
const teamsAppId = item.get("writeToEnvironmentFile")?.get("teamsAppId") as string;
if (teamsAppId) {
const envName = Utils.getSafeRegistrationIdEnvName(`${authName}_REGISTRATION_ID`);
provisionNode.items.splice(i + 1, 0, {
uses: "apiKey/register",
with: {
name: `${authName}`,
appId: `\${{${teamsAppId}}}`,
apiSpecPath: specRelativePath,
},
writeToEnvironmentFile: {
registrationId: envName,
},
});
added = true;
break;
}
}
}

if (!added) {
throw new InjectAPIKeyActionFailedError();
}

await fs.writeFile(ymlPath, document.toString(), "utf8");
}
} else {
throw new InjectAPIKeyActionFailedError();
}
}

@hooks([
ErrorContextMW({ component: "FxCore", stage: "copilotPluginAddAPI" }),
ErrorHandlerMW,
Expand Down Expand Up @@ -1371,43 +1317,54 @@ export class FxCore {
);

try {
// TODO: type b will support auth
if (!isPlugin) {
const authNames: Set<string> = new Set();
const serverUrls: Set<string> = new Set();
for (const api of operations) {
const operation = apiResultList.find((op) => op.api === api);
if (
operation &&
operation.auth &&
(Utils.isBearerTokenAuth(operation.auth.authScheme) ||
Utils.isOAuthWithAuthCodeFlow(operation.auth.authScheme))
) {
authNames.add(operation.auth.name);
serverUrls.add(operation.server);
}
const authNames: Set<string> = new Set();
const serverUrls: Set<string> = new Set();
let authScheme: OpenAPIV3.SecuritySchemeObject | undefined = undefined;
for (const api of operations) {
const operation = apiResultList.find((op) => op.api === api);
if (
operation &&
operation.auth &&
(Utils.isBearerTokenAuth(operation.auth.authScheme) ||
Utils.isOAuthWithAuthCodeFlow(operation.auth.authScheme))
) {
authNames.add(operation.auth.name);
serverUrls.add(operation.server);
authScheme = operation.auth.authScheme;
}
}

if (authNames.size > 1) {
throw new MultipleAuthError(authNames);
}
if (authNames.size > 1) {
throw new MultipleAuthError(authNames);
}

if (serverUrls.size > 1) {
throw new MultipleServerError(serverUrls);
}
if (serverUrls.size > 1) {
throw new MultipleServerError(serverUrls);
}

if (authNames.size === 1 && authScheme) {
const ymlPath = path.join(inputs.projectPath!, MetadataV3.configFile);
const localYamlPath = path.join(inputs.projectPath!, MetadataV3.localConfigFile);
const authName = [...authNames][0];

if (authNames.size === 1) {
const ymlPath = path.join(inputs.projectPath!, MetadataV3.configFile);
const localYamlPath = path.join(inputs.projectPath!, MetadataV3.localConfigFile);
const authName = [...authNames][0];
const relativeSpecPath =
"./" + path.relative(inputs.projectPath!, outputApiSpecPath).replace(/\\/g, "/");

const relativeSpecPath =
"./" + path.relative(inputs.projectPath!, outputApiSpecPath).replace(/\\/g, "/");
if (Utils.isBearerTokenAuth(authScheme)) {
await ActionInjector.injectCreateAPIKeyAction(ymlPath, authName, relativeSpecPath);

await this.injectCreateAPIKeyAction(ymlPath, authName, relativeSpecPath);
if (await fs.pathExists(localYamlPath)) {
await ActionInjector.injectCreateAPIKeyAction(
localYamlPath,
authName,
relativeSpecPath
);
}
} else if (Utils.isOAuthWithAuthCodeFlow(authScheme)) {
await ActionInjector.injectCreateOAuthAction(ymlPath, authName, relativeSpecPath);

if (await fs.pathExists(localYamlPath)) {
await this.injectCreateAPIKeyAction(localYamlPath, authName, relativeSpecPath);
await ActionInjector.injectCreateOAuthAction(localYamlPath, authName, relativeSpecPath);
}
}
}
Expand Down
11 changes: 11 additions & 0 deletions packages/fx-core/src/error/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,17 @@ export class InjectAPIKeyActionFailedError extends UserError {
}
}

export class InjectOAuthActionFailedError extends UserError {
constructor() {
super({
message: getDefaultString("core.copilot.addAPI.InjectOAuthActionFailed"),
displayMessage: getLocalizedString("core.copilot.addAPI.InjectOAuthActionFailed"),
source: "coordinator",
categories: [ErrorCategory.Internal],
});
}
}

export class JSONSyntaxError extends UserError {
constructor(filePathOrContent: string, error: any, source?: string) {
super({
Expand Down
Loading

0 comments on commit 0d4f61e

Please sign in to comment.