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(auth): add addAuthAction command in core #13046

Merged
merged 4 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
InputsWithProjectPath,
ManifestUtil,
Platform,
PluginManifestSchema,
ResponseTemplatesFolderName,
Result,
Stage,
Expand Down Expand Up @@ -2228,6 +2229,61 @@
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

Check warning on line 2240 in packages/fx-core/src/core/FxCore.ts

View check run for this annotation

Codecov / codecov/patch

packages/fx-core/src/core/FxCore.ts#L2240

Added line #L2240 was not covered by tests
}

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
Loading