Skip to content

Commit

Permalink
perf(auth): add addAuthAction command in core (#13046)
Browse files Browse the repository at this point in the history
* perf(auth): add addAuthAction command in core

* fix: update api

* test: add ut

* test: add ut
  • Loading branch information
KennethBWSong authored Jan 15, 2025
1 parent 2224cef commit ad64394
Show file tree
Hide file tree
Showing 12 changed files with 722 additions and 9 deletions.
1 change: 1 addition & 0 deletions packages/api/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export enum Stage {
syncManifest = "syncManifest",
addPlugin = "addPlugin",
kiotaRegenerate = "kiotaRegenerate",
addAuthAction = "addAuthAction",
}

export enum TelemetryEvent {
Expand Down
7 changes: 6 additions & 1 deletion packages/fx-core/resource/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -932,5 +932,10 @@
"error.dep.VxTestAppValidationError": "Unable to validate video extensibility test app after installation.",
"error.dep.FindProcessError": "Unable to find process(es) by pid or port. %s",
"error.kiota.FailedToCreateAdaptiveCard": "Unable to generate adaptive card in plugin manifest. Manually update the manifest file, if required.",
"error.kiota.FailedToGenerateAuthActions": "Unable to parse Open API spec and generate Auth actions in teamsapp.yml. Manually update the yml files, if required."
"error.kiota.FailedToGenerateAuthActions": "Unable to parse Open API spec and generate Auth actions in teamsapp.yml. Manually update the yml files, if required.",
"core.addAuthActionQuestion.ApiSpecLocation.title": "Select an OpenAPI Description Document",
"core.addAuthActionQuestion.ApiSpecLocation.placeholder": "Select an option",
"core.addAuthActionQuestion.ApiOperation.title": "Select an OpenAPI Description Document",
"core.addAuthActionQuestion.ApiOperation.placeholder": "Select an option",
"core.addAuthActionQuestion.authName.title": "Input the Name of Auth Configuration"
}
12 changes: 8 additions & 4 deletions packages/fx-core/src/component/generator/apiSpec/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,16 +601,17 @@ export function logValidationResults(
export async function injectAuthAction(
projectPath: string,
authName: string,
authScheme: AuthType,
authScheme: AuthType | undefined,
outputApiSpecPath: string,
forceToAddNew: boolean
forceToAddNew: boolean,
authType?: string
): Promise<AuthActionInjectResult | undefined> {
const ymlPath = path.join(projectPath, MetadataV3.configFile);
const localYamlPath = path.join(projectPath, MetadataV3.localConfigFile);

const relativeSpecPath = "./" + path.relative(projectPath, outputApiSpecPath).replace(/\\/g, "/");

if (Utils.isBearerTokenAuth(authScheme)) {
if ((!!authScheme && Utils.isBearerTokenAuth(authScheme)) || authType === "ApiKeyPluginVault") {
const res = await ActionInjector.injectCreateAPIKeyAction(
ymlPath,
authName,
Expand All @@ -627,7 +628,10 @@ export async function injectAuthAction(
);
}
return res;
} else if (Utils.isOAuthWithAuthCodeFlow(authScheme)) {
} else if (
(!!authScheme && Utils.isOAuthWithAuthCodeFlow(authScheme)) ||
authType === "OAuth2PluginVault"
) {
const res = await ActionInjector.injectCreateOAuthAction(
ymlPath,
authName,
Expand Down
56 changes: 56 additions & 0 deletions packages/fx-core/src/core/FxCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
InputsWithProjectPath,
ManifestUtil,
Platform,
PluginManifestSchema,
ResponseTemplatesFolderName,
Result,
Stage,
Expand Down Expand Up @@ -2228,6 +2229,61 @@ export class FxCore {
return ok(undefined);
}

@hooks([
ErrorContextMW({ component: "FxCore", stage: Stage.addAuthAction }),
ErrorHandlerMW,
QuestionMW("addAuthAction"),
ConcurrentLockerMW,
])
async addAuthAction(inputs: Inputs): Promise<Result<undefined, FxError>> {
if (!inputs.projectPath) {
throw new Error("projectPath is undefined"); // should never happen
}

const pluginManifestPath = inputs[QuestionNames.PluginManifestFilePath] as string;
const apiSpecRelativePath = inputs[QuestionNames.ApiSpecLocation] as string;
const apiOperation = inputs[QuestionNames.ApiOperation] as string[];
const authName = inputs[QuestionNames.AuthName] as string;
const apiSpecPath = path.normalize(path.join(pluginManifestPath, apiSpecRelativePath));
let authType;
switch (inputs[QuestionNames.ApiAuth] as string) {
case "api-key":
default:
authType = "ApiKeyPluginVault";
break;
case "oauth":
authType = "OAuthPluginVault";
break;
}

const addAuthActionRes = await injectAuthAction(
inputs.projectPath,
authName,
undefined,
apiSpecPath,
true,
authType
);

if (addAuthActionRes?.registrationIdEnvName) {
const pluginManifest = (await fs.readJson(pluginManifestPath)) as PluginManifestSchema;
pluginManifest.runtimes?.push({
type: "OpenApi",
auth: {
type: authType as "None" | "OAuthPluginVault" | "ApiKeyPluginVault",
reference_id: `\$\{\{${addAuthActionRes.registrationIdEnvName}\}\}`,
},
spec: {
url: apiSpecRelativePath,
run_for_functions: apiOperation,
},
});
await fs.writeJson(pluginManifestPath, pluginManifest, { spaces: 4 });
}

return ok(undefined);
}

private async updateAuthActionInYaml(
authName: string | undefined,
authScheme: AuthType | undefined,
Expand Down
2 changes: 2 additions & 0 deletions packages/fx-core/src/question/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export enum QuestionNames {
ImportPlugin = "import-plugin",
PluginManifestFilePath = "plugin-manifest-path",
PluginOpenApiSpecFilePath = "plugin-opeanapi-spec-path",

AuthName = "auth-name",
}

export enum ProjectTypeGroup {
Expand Down
4 changes: 2 additions & 2 deletions packages/fx-core/src/question/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -938,7 +938,7 @@ export function apiSpecLocationQuestion(includeExistingAPIs = true): SingleFileO
};
}

export function apiAuthQuestion(): SingleSelectQuestion {
export function apiAuthQuestion(excludeNone = false): SingleSelectQuestion {
return {
type: "singleSelect",
name: QuestionNames.ApiAuth,
Expand All @@ -949,7 +949,7 @@ export function apiAuthQuestion(): SingleSelectQuestion {
cliDescription: "The authentication type for the API.",
staticOptions: ApiAuthOptions.all(),
dynamicOptions: (inputs: Inputs) => {
const options: OptionItem[] = [ApiAuthOptions.none()];
const options: OptionItem[] = excludeNone ? [] : [ApiAuthOptions.none()];
if (inputs[QuestionNames.MeArchitectureType] === MeArchitectureOptions.newApi().id) {
options.push(ApiAuthOptions.bearerToken(), ApiAuthOptions.microsoftEntra());
} else if (inputs[QuestionNames.ApiPluginType] === ApiPluginStartOptions.newApi().id) {
Expand Down
4 changes: 4 additions & 0 deletions packages/fx-core/src/question/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
syncManifestQuestionNode,
kiotaRegenerateQuestion,
convertAadToNewSchemaQuestionNode,
addAuthActionQuestion,
} from "./other";
export * from "./constants";
export * from "./create";
Expand Down Expand Up @@ -88,6 +89,9 @@ export class QuestionNodes {
kiotaRegenerate(): IQTreeNode {
return kiotaRegenerateQuestion();
}
addAuthAction(): IQTreeNode {
return addAuthActionQuestion();
}
}

export const questionNodes = new QuestionNodes();
108 changes: 108 additions & 0 deletions packages/fx-core/src/question/other.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
TextInputQuestion,
FolderQuestion,
CLIPlatforms,
PluginManifestSchema,
} from "@microsoft/teamsfx-api";
import fs from "fs-extra";
import * as path from "path";
Expand All @@ -37,6 +38,7 @@ import {
SPFxFrameworkQuestion,
SPFxImportFolderQuestion,
SPFxWebpartNameQuestion,
apiAuthQuestion,
apiOperationQuestion,
apiPluginStartQuestion,
apiSpecLocationQuestion,
Expand Down Expand Up @@ -813,6 +815,112 @@ export function kiotaRegenerateQuestion(): IQTreeNode {
};
}

export function addAuthActionQuestion(): IQTreeNode {
return {
data: pluginManifestQuestion(),
children: [
{
data: apiSpecFromPluginManifestQuestion(),
condition: async (inputs: Inputs) => {
const pluginManifestPath = inputs[QuestionNames.PluginManifestFilePath];
const pluginManifest = (await fs.readJson(pluginManifestPath)) as PluginManifestSchema;
const specs = pluginManifest
.runtimes!.filter((runtime) => runtime.type === "OpenApi")
.map((runtime) => runtime.spec.url);
if (specs.length === 1) {
inputs[QuestionNames.ApiSpecLocation] = specs[0];
return false;
}
return true;
},
},
{
data: apiFromPluginManifestQuestion(),
condition: async (inputs: Inputs) => {
const pluginManifestPath = inputs[QuestionNames.PluginManifestFilePath];
const apiSpecPath = inputs[QuestionNames.ApiSpecLocation];
const pluginManifest = (await fs.readJson(pluginManifestPath)) as PluginManifestSchema;
const apis: string[] = [];
pluginManifest
.runtimes!.filter(
(runtime) => runtime.type === "OpenApi" && runtime.spec.url === apiSpecPath
)
.forEach((runtime) => {
apis.push(...(runtime.run_for_functions as string[]));
});
if (apis.length === 1) {
inputs[QuestionNames.ApiOperation] = apis;
return false;
}
return true;
},
},
{
data: authNameQuestion(),
},
{
data: apiAuthQuestion(true),
},
],
};
}

export function apiSpecFromPluginManifestQuestion(): SingleSelectQuestion {
return {
name: QuestionNames.ApiSpecLocation,
title: getLocalizedString("core.addAuthActionQuestion.ApiSpecLocation.title"),
placeholder: getLocalizedString("core.addAuthActionQuestion.ApiSpecLocation.placeholder"),
type: "singleSelect",
staticOptions: [],
dynamicOptions: async (inputs: Inputs) => {
const pluginManifestPath = inputs[QuestionNames.PluginManifestFilePath];
const pluginManifest = (await fs.readJson(pluginManifestPath)) as PluginManifestSchema;
const specs = pluginManifest
.runtimes!.filter((runtime) => runtime.type === "OpenApi")
.map((runtime) => runtime.spec.url as string);
return specs;
},
};
}

export function apiFromPluginManifestQuestion(): MultiSelectQuestion {
return {
name: QuestionNames.ApiOperation,
title: getLocalizedString("core.addAuthActionQuestion.ApiOperation.title"),
type: "multiSelect",
staticOptions: [],
placeholder: getLocalizedString("core.addAuthActionQuestion.ApiOperation.placeholder"),
dynamicOptions: async (inputs: Inputs) => {
const pluginManifestPath = inputs[QuestionNames.PluginManifestFilePath];
const apiSpecPath = inputs[QuestionNames.ApiSpecLocation];
const pluginManifest = (await fs.readJson(pluginManifestPath)) as PluginManifestSchema;
const apis = pluginManifest
.runtimes!.filter(
(runtime) => runtime.type === "OpenApi" && runtime.spec.url === apiSpecPath
)
.map((runtime) => runtime.spec.run_for_functions as string);
return apis;
},
};
}

export function authNameQuestion(): TextInputQuestion {
return {
name: QuestionNames.AuthName,
title: getLocalizedString("core.addAuthActionQuestion.authName.title"),
type: "text",
additionalValidationOnAccept: {
validFunc: (input: string, inputs?: Inputs): string | undefined => {
if (!inputs) {
throw new Error("inputs is undefined"); // should never happen
}
inputs[QuestionNames.ApiPluginType] = ApiPluginStartOptions.newApi().id;
return;
},
},
};
}

export function apiSpecApiKeyConfirmQestion(): ConfirmQuestion {
return {
name: QuestionNames.ApiSpecApiKeyConfirm,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,39 @@ describe("injectAuthAction", async () => {
assert.isUndefined(res);
assert.isTrue(injectStub.calledOnce);
});

it("api key auth from authType", async () => {
sandbox.stub(fs, "pathExists").resolves(true);
// sandbox.stub(Utils, "isBearerTokenAuth").returns(true);
const injectStub = sandbox.stub(ActionInjector, "injectCreateAPIKeyAction").resolves(undefined);
const res = await injectAuthAction(
"oauth",
"test",
undefined,
"test",
false,
"ApiKeyPluginVault"
);

assert.isUndefined(res);
assert.isTrue(injectStub.calledTwice);
});

it("oauth auth from authType", async () => {
sandbox.stub(fs, "pathExists").resolves(true);
const injectStub = sandbox.stub(ActionInjector, "injectCreateOAuthAction").resolves(undefined);
const res = await injectAuthAction(
"oauth",
"test",
undefined,
"test",
false,
"OAuth2PluginVault"
);

assert.isUndefined(res);
assert.isTrue(injectStub.calledTwice);
});
});

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

0 comments on commit ad64394

Please sign in to comment.