Skip to content

feat: move adding agent owner to collaborator #14384

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

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from
Open
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
50 changes: 45 additions & 5 deletions packages/cli/src/commands/models/permissionGrant.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { CLICommand, InputsWithProjectPath, err, ok } from "@microsoft/teamsfx-api";
import { PermissionGrantInputs, PermissionGrantOptions } from "@microsoft/teamsfx-core";
import {
CLICommand,
CLICommandOption,
err,
InputsWithProjectPath,
ok,
} from "@microsoft/teamsfx-api";
import {
CollaborationConstants,
featureFlagManager,
FeatureFlags,
PermissionGrantInputs,
PermissionGrantOptions,
QuestionNames,
} from "@microsoft/teamsfx-core";
import { getFxCore } from "../../activate";
import { logger } from "../../commonlib/logger";
import { MissingRequiredOptionError } from "../../error";
Expand All @@ -20,10 +33,21 @@ export const spfxMessage =
"Manage site admins using SharePoint admin center: " +
"https://docs.microsoft.com/en-us/sharepoint/manage-site-collection-administrators";

export const agentOwnerOption: CLICommandOption = {
name: "agent",
type: "boolean",
default: false,
description: "Whether share the ownership of agent.",
};

export const permissionGrantCommand: CLICommand = {
name: "grant",
description: commands["collaborator.grant"].description,
options: [...PermissionGrantOptions, ProjectFolderOption],
options: [
...PermissionGrantOptions,
ProjectFolderOption,
...(featureFlagManager.getBooleanValue(FeatureFlags.ShareEnabled) ? [agentOwnerOption] : []),
],
telemetry: {
event: TelemetryEvent.GrantPermission,
},
Expand All @@ -32,18 +56,34 @@ export const permissionGrantCommand: CLICommand = {
command: `${process.env.TEAMSFX_CLI_BIN_NAME} collaborator grant -i false --manifest-file ./appPackage/manifest.json --env dev --email [email protected]`,
description: "Grant permission for another Microsoft 365 account to collaborate on the app.",
},
...(featureFlagManager.getBooleanValue(FeatureFlags.ShareEnabled)
? [
{
command: `${process.env.TEAMSFX_CLI_BIN_NAME} collaborator grant -i false --agent true --env dev --email [email protected]`,
description:
"Grant permission for another Microsoft 365 account as owner of the agent.",
},
]
: []),
],
handler: async (ctx) => {
const inputs = ctx.optionValues as PermissionGrantInputs & InputsWithProjectPath;
// print necessary messages
logger.info(azureMessage);
logger.info(spfxMessage);
if (ctx.optionValues["agent"]) {
inputs[QuestionNames.collaborationAppType] = [CollaborationConstants.AgentOptionId];
}
if (!ctx.globalOptionValues.interactive) {
if (!inputs["manifest-file-path"] && !inputs["manifest-path"]) {
if (
!ctx.optionValues["agent"] &&
!inputs["entra-app-manifest-file"] &&
!inputs["manifest-path"]
) {
return err(
new MissingRequiredOptionError(
ctx.command.fullName,
"--manifest-file-path or --manifest-path"
"--entra-app-manifest-file or --manifest-path"
)
);
}
Expand Down
17 changes: 14 additions & 3 deletions packages/cli/src/commands/models/permissionStatus.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { CLICommand, InputsWithProjectPath, err, ok } from "@microsoft/teamsfx-api";
import { PermissionListInputs, PermissionListOptions } from "@microsoft/teamsfx-core";
import { CLICommand, err, InputsWithProjectPath, ok } from "@microsoft/teamsfx-api";
import {
CollaborationConstants,
featureFlagManager,
FeatureFlags,
PermissionListInputs,
PermissionListOptions,
QuestionNames,
} from "@microsoft/teamsfx-core";
import { getFxCore } from "../../activate";
import { logger } from "../../commonlib/logger";
import { commands } from "../../resource";
import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents";
import { ProjectFolderOption } from "../common";
import { azureMessage, spfxMessage } from "./permissionGrant";
import { agentOwnerOption, azureMessage, spfxMessage } from "./permissionGrant";

export const permissionStatusCommand: CLICommand = {
name: "status",
Expand All @@ -21,6 +28,7 @@ export const permissionStatusCommand: CLICommand = {
type: "boolean",
required: false,
},
...(featureFlagManager.getBooleanValue(FeatureFlags.ShareEnabled) ? [agentOwnerOption] : []),
ProjectFolderOption,
],
telemetry: {
Expand All @@ -34,6 +42,9 @@ export const permissionStatusCommand: CLICommand = {
// print necessary messages
logger.info(azureMessage);
logger.info(spfxMessage);
if (ctx.optionValues["agent"]) {
inputs[QuestionNames.collaborationAppType] = [CollaborationConstants.AgentOptionId];
}
const result = listAll
? await core.listCollaborator(inputs)
: await core.checkPermission(inputs);
Expand Down
10 changes: 5 additions & 5 deletions packages/cli/src/commands/models/root.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { CLICommand, ok } from "@microsoft/teamsfx-api";
import { featureFlagManager, FeatureFlags } from "@microsoft/teamsfx-core";
import { logger } from "../../commonlib/logger";
import { FooterText } from "../../constants";
import { commands } from "../../resource";
import { TelemetryEvent } from "../../telemetry/cliTelemetryEvents";
import { getVersion } from "../../utils";
import { helper } from "../helper";
Expand All @@ -19,17 +21,15 @@ import { m365UnacquireCommand } from "./m365Unacquire";
import { permissionCommand } from "./permission";
import { previewCommand } from "./preview";
import { provisionCommand } from "./provision";
import { regenerateCommand } from "./regnereate";
import { setCommand } from "./set";
import { shareCommand } from "./share";
import { teamsappDoctorCommand } from "./teamsapp/doctor";
import { teamsappPackageCommand } from "./teamsapp/package";
import { teamsappPublishCommand } from "./teamsapp/publish";
import { teamsappUpdateCommand } from "./teamsapp/update";
import { teamsappValidateCommand } from "./teamsapp/validate";
import { upgradeCommand } from "./upgrade";
import { commands } from "../../resource";
import { shareCommand } from "./share";
import { setCommand } from "./set";
import { featureFlagManager, FeatureFlags } from "@microsoft/teamsfx-core";
import { regenerateCommand } from "./regnereate";

export const helpCommand: CLICommand = {
name: "help",
Expand Down
4 changes: 0 additions & 4 deletions packages/cli/src/commands/models/share.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,6 @@ export const shareCommand: CLICommand = {
command: `${process.env.TEAMSFX_CLI_BIN_NAME} share --scope users --email '[email protected],[email protected]' -i false`,
description: "Share the agent with specific users",
},
{
command: `${process.env.TEAMSFX_CLI_BIN_NAME} share --scope owners --email '[email protected],[email protected]' -i false`,
description: "Share the ownership of agent with selected users",
},
],
commands: [shareRemoveCommand],
};
117 changes: 116 additions & 1 deletion packages/cli/tests/unit/commands.tests.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { CLIContext, SystemError, err, ok, signedIn, signedOut } from "@microsoft/teamsfx-api";
import {
CollaborationConstants,
CollaborationStateResult,
FuncToolChecker,
FxCore,
ListCollaboratorResult,
LocalCertificateManager,
LtsNodeChecker,
PackageService,
PermissionGrantInputs,
PermissionListInputs,
PermissionsResult,
QuestionNames,
UserCancelError,
envUtil,
featureFlagManager,
Expand Down Expand Up @@ -482,20 +486,85 @@ describe("CLI commands", () => {
});
});
describe("permissionGrantCommand", async () => {
afterEach(() => {
sandbox.restore();
});

it("success with agent option", async () => {
sandbox.stub(featureFlagManager, "getBooleanValue").returns(true);
sandbox
.stub(FxCore.prototype, "grantPermission")
.resolves(ok({ state: "OK" } as PermissionsResult));
const ctx: CLIContext = {
command: { ...permissionGrantCommand, fullName: "teamsfx" },
optionValues: { agent: true, email: "email", env: "dev" },
globalOptionValues: { interactive: false },
argumentValues: [],
telemetryProperties: {},
};
const res = await permissionGrantCommand.handler!(ctx);
assert.isTrue(res.isOk());
const inputs = ctx.optionValues as PermissionGrantInputs;
assert.deepEqual(inputs[QuestionNames.collaborationAppType], [
CollaborationConstants.AgentOptionId,
]);
});

it("success with agent option in interactive mode", async () => {
sandbox.stub(featureFlagManager, "getBooleanValue").returns(true);
sandbox
.stub(FxCore.prototype, "grantPermission")
.resolves(ok({ state: "OK" } as PermissionsResult));
const ctx: CLIContext = {
command: { ...permissionGrantCommand, fullName: "teamsfx" },
optionValues: { agent: true },
globalOptionValues: { interactive: true },
argumentValues: [],
telemetryProperties: {},
};
const res = await permissionGrantCommand.handler!(ctx);
assert.isTrue(res.isOk());
const inputs = ctx.optionValues as PermissionGrantInputs;
assert.deepEqual(inputs[QuestionNames.collaborationAppType], [
CollaborationConstants.AgentOptionId,
]);
});

it("missing manifest options with agent = false", async () => {
sandbox.stub(featureFlagManager, "getBooleanValue").returns(true);
sandbox
.stub(FxCore.prototype, "grantPermission")
.resolves(ok({ state: "OK" } as PermissionsResult));
const ctx: CLIContext = {
command: { ...permissionGrantCommand, fullName: "teamsfx" },
optionValues: { env: "dev", email: "email", agent: false },
globalOptionValues: { interactive: false },
argumentValues: [],
telemetryProperties: {},
};
const res = await permissionGrantCommand.handler!(ctx);
assert.isTrue(res.isErr());
if (res.isErr()) {
assert.isTrue(res.error instanceof MissingRequiredOptionError);
}
});

it("success interactive = false", async () => {
sandbox.stub(featureFlagManager, "getBooleanValue").returns(false);
sandbox
.stub(FxCore.prototype, "grantPermission")
.resolves(ok({ state: "OK" } as PermissionsResult));
const ctx: CLIContext = {
command: { ...permissionGrantCommand, fullName: "teamsfx" },
optionValues: { "manifest-file-path": "abc" },
optionValues: { "manifest-path": "abc" },
globalOptionValues: { interactive: false },
argumentValues: [],
telemetryProperties: {},
};
const res = await permissionGrantCommand.handler!(ctx);
assert.isTrue(res.isOk());
});

it("success interactive = true", async () => {
sandbox
.stub(FxCore.prototype, "grantPermission")
Expand Down Expand Up @@ -526,7 +595,52 @@ describe("CLI commands", () => {
});
});
describe("permissionStatusCommand", async () => {
afterEach(() => {
sandbox.restore();
});

it("listCollaborator with agent option", async () => {
sandbox.stub(featureFlagManager, "getBooleanValue").returns(true);
sandbox
.stub(FxCore.prototype, "listCollaborator")
.resolves(ok({ state: "OK" } as ListCollaboratorResult));
const ctx: CLIContext = {
command: { ...permissionStatusCommand, fullName: "teamsfx" },
optionValues: { all: true, agent: true },
globalOptionValues: {},
argumentValues: [],
telemetryProperties: {},
};
const res = await permissionStatusCommand.handler!(ctx);
assert.isTrue(res.isOk());
const inputs = ctx.optionValues as PermissionListInputs;
assert.deepEqual(inputs[QuestionNames.collaborationAppType], [
CollaborationConstants.AgentOptionId,
]);
});

it("checkPermission with agent option", async () => {
sandbox.stub(featureFlagManager, "getBooleanValue").returns(true);
sandbox
.stub(FxCore.prototype, "checkPermission")
.resolves(ok({ state: "OK" } as CollaborationStateResult));
const ctx: CLIContext = {
command: { ...permissionStatusCommand, fullName: "teamsfx" },
optionValues: { all: false, agent: true },
globalOptionValues: {},
argumentValues: [],
telemetryProperties: {},
};
const res = await permissionStatusCommand.handler!(ctx);
assert.isTrue(res.isOk());
const inputs = ctx.optionValues as PermissionListInputs;
assert.deepEqual(inputs[QuestionNames.collaborationAppType], [
CollaborationConstants.AgentOptionId,
]);
});

it("listCollaborator", async () => {
sandbox.stub(featureFlagManager, "getBooleanValue").returns(false);
sandbox
.stub(FxCore.prototype, "listCollaborator")
.resolves(ok({ state: "OK" } as ListCollaboratorResult));
Expand All @@ -540,6 +654,7 @@ describe("CLI commands", () => {
const res = await permissionStatusCommand.handler!(ctx);
assert.isTrue(res.isOk());
});

it("checkPermission", async () => {
sandbox
.stub(FxCore.prototype, "checkPermission")
Expand Down
7 changes: 5 additions & 2 deletions packages/fx-core/resource/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,20 @@
"core.collaboration.AccountUsedToCheck": "Account used to check: ",
"core.collaboration.StartingListAllTeamsAppOwners": "\nStarting to list all app owners for environment: ",
"core.collaboration.StartingListAllAadAppOwners": "\nStarting to list all Microsoft Entra app owners for environment: ",
"core.collaboration.StartingListAllAgentOwners": "\nStarting to list all agent owners for environment: ",
"core.collaboration.M365TeamsAppId": "App (ID: ",
"core.collaboration.SsoAadAppId": "SSO Microsoft Entra App (ID: ",
"core.collaboration.AgentTitleId": "Agent (ID: ",
"core.collaboration.TeamsAppOwner": "App Owner: ",
"core.collaboration.AadAppOwner": "Microsoft Entra App Owner: ",
"core.collaboration.AgentOwner": "Agent Owner: ",
"core.collaboration.StaringCheckPermission": "Starting to check permission for environment: ",
"core.collaboration.CheckPermissionResourceId": "Resource ID: ",
"core.collaboration.Undefined": "undefined",
"core.collaboration.ResourceName": ", Resource Name: ",
"core.collaboration.Permission": ", Permission: ",
"core.collaboration.agent.label": "Agent",
"core.collaboration.agent.description": "Microsoft 365 Agent",
"core.developerPortal.scaffold.CannotFindManifest": "Manifest not found from the downloaded package for app %s.",
"plugins.spfx.questions.framework.title": "Framework",
"plugins.spfx.questions.webpartName": "Name for SharePoint Framework Web Part",
Expand Down Expand Up @@ -641,7 +646,6 @@
"core.common.ReceiveApiResponse": "Received API response: %s.",
"core.common.shareWithTenant.success": "Agent successfully shared with tenant.",
"core.common.shareWithUser.success": "Agent successfully shared with users: %s.",
"core.common.shareWithOwner.success": "Agent successfully shared with owners: %s.",
"core.common.removeOwnership.success": "Shared agent ownership removed from the users: %s.",
"core.common.removeShareAccess.success": "Shared access successfully removed from users: %s.",
"core.envFunc.unsupportedFile.errorLog": "\"%s\" is an invalid file. Supported format: %s.",
Expand Down Expand Up @@ -702,7 +706,6 @@
"core.share.removeAccess.operator": "Cannot remove permission of the operator. Email: %s.",
"core.shareOperationQuestion.option.removeShareAccessFromUsers": "Remove access for selected user(s)",
"core.shareOperationQuestion.option.shareWithUsers": "Share access with selected user(s)",
"core.shareOptionQuestion.option.shareWithOwners": "Share agent ownership with selected user(s)",
"core.shareOptionQuestion.placeholder": "Select how to share the agent",
"core.shareOptionQuestion.title": "Share the agent",
"core.shareOptionQuestion.unshare.emails.title": "Email addresses of users for agent access removal",
Expand Down
Loading