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

perf(type-b): update add new api logic to support oauth #11515

Merged
merged 5 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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: 43 additions & 84 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 @@ -109,6 +110,7 @@ import { settingsUtil } from "../component/utils/settingsUtil";
import {
FileNotFoundError,
InjectAPIKeyActionFailedError,
InjectOAuthActionFailedError,
InputValidationError,
InvalidProjectError,
MissingRequiredInputError,
Expand Down Expand Up @@ -149,6 +151,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 +1243,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 +1319,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