Repository navigation
Add a365 network gsa enable|disable|status #497
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
Lala Sushant Srivastava (lasrivas)
wants to merge
1
commit into
main
Choose a base branch
from
feature/gsa-environment-setting
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| # `a365 network gsa` | ||
|
|
||
| Turns **Global Secure Access** on or off for the tenant's Agent 365 Power Platform environment, | ||
| without needing the id of that environment. | ||
|
|
||
| ## Why this command exists | ||
|
|
||
| Global Secure Access is a per-environment Power Platform setting. Agent 365 provisions a managed | ||
| environment for the tenant and does not publish its id, so the setting cannot be reached through | ||
| the Power Platform admin surfaces that take an environment id. These subcommands ask the Agent 365 | ||
| platform to apply the change against the environment it resolves for your tenant. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - **Global Administrator** or **Power Platform Administrator** in the tenant. The platform rejects | ||
| anyone else. | ||
| - An `az login --tenant <id> --allow-no-subscriptions` to the tenant you intend to configure; | ||
| no Azure subscription is needed. | ||
| - Public cloud only. Sovereign cloud configurations are rejected before authentication. | ||
| - The Agent 365 Tools API must expose `AgentTools.Gsa.Manage.All` for enable/disable and | ||
| `AgentTools.Gsa.Read.All` for status, and preauthorize them for the CLI's Microsoft client application. | ||
| These are platform deployment prerequisites, not permissions an admin can add to Microsoft's app registration. | ||
|
|
||
| The `az login` is what selects the tenant. These commands read `az account show` once per run and | ||
| authenticate against the tenant and account it reports, so `az login --tenant <id>` is how you | ||
| choose which tenant to configure when you have more than one. Nothing else is read from Azure — the | ||
| setting itself lives in Power Platform, not in your subscription. Without an explicit tenant the | ||
| Windows broker silently returns whichever account Windows prefers, which would apply a tenant-wide | ||
| setting to the wrong tenant. If the account you are signed into cannot be matched, the command | ||
| fails rather than falling back. | ||
|
|
||
| ## Subcommands | ||
|
|
||
| | Command | Description | | ||
| | --- | --- | | ||
| | `a365 network gsa enable` | Turn Global Secure Access on. | | ||
| | `a365 network gsa disable` | Turn Global Secure Access off. | | ||
| | `a365 network gsa status` | Show whether Global Secure Access is on. | | ||
|
|
||
| ### `enable` and `disable` | ||
|
|
||
| ```bash | ||
| a365 network gsa enable [--wait] [--yes] | ||
| a365 network gsa disable [--wait] [--yes] | ||
| ``` | ||
|
|
||
| | Option | Description | | ||
| | --- | --- | | ||
| | `--wait` | Keep polling until the change appears on the environment, instead of returning while it is still being applied. | | ||
| | `--yes`, `-y` | Skip the confirmation prompt. | | ||
|
|
||
| Both verbs prompt before changing the tenant-wide setting; pass `--yes` in automation. | ||
| Requesting the value the environment already holds is a no-op and succeeds. | ||
|
|
||
| ### `status` | ||
|
|
||
| ```bash | ||
| a365 network gsa status | ||
| ``` | ||
|
|
||
| There is no operation handle to pass. Power Platform applies the change asynchronously but issues | ||
| no operation id for it, so the CLI reports progress by re-reading the setting rather than by | ||
| polling a handle. | ||
|
|
||
| ## Statuses and exit codes | ||
|
|
||
| | Status | Meaning | | ||
| | --- | --- | | ||
| | `Enabled` | Global Secure Access is on. | | ||
| | `Disabled` | Global Secure Access is off. | | ||
| | `NotConfigured` | The tenant has never set the value. This is **not** the same as `Disabled`. | | ||
|
|
||
| A change that has been accepted but has not yet surfaced is reported as still being applied, with | ||
| the status still showing the value it has not yet displaced. | ||
|
|
||
| Without `--wait`, an accepted change returns `0` even while it is still being applied; output labels | ||
| the requested and current values separately. With `--wait`, `0` means the requested value was observed. | ||
| A wait that cannot confirm that value within ten minutes returns `1`, names the last observed value | ||
| when available, and tells you to check `a365 network gsa status`. The change remains accepted; | ||
| the timeout does not cancel it. | ||
|
|
||
| Transient status-read failures are retried up to three attempts with bounded backoff, within the | ||
| wait's overall time budget. Permanent request errors and exhausted retries return `1`; if a change | ||
| was already accepted, the CLI reports that separately from a failure to verify its status. | ||
|
|
||
| ## Typical flow | ||
|
|
||
| ```bash | ||
| a365 network gsa enable --wait | ||
| a365 network gsa status | ||
| ``` | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| | Symptom | Cause | | ||
| | --- | --- | | ||
| | `403`, reporting `insufficient_scope` | The token lacks the reported scope. Contact support to verify the API exposes it and preauthorizes it for the CLI's Microsoft client application. | | ||
| | Other `403` from the platform | Check the reported error and confirm the caller has the Global Administrator or Power Platform Administrator role. | | ||
| | `409`, reporting a governing policy | A Power Platform policy owns this setting. Change it through that policy; the environment-level value is ignored while the policy applies. | | ||
| | `404`, reporting no environment | The tenant has no Agent 365 environment yet. | | ||
| | Status stays `NotConfigured` after `disable` | Read it again — the change is applied asynchronously and `--wait` is the way to block on it. | | ||
| | `Could not determine your Azure tenant` | No usable `az login`. Run `az login --tenant <id> --allow-no-subscriptions` for the tenant you want to configure. | | ||
| | Public-cloud-only error | The selected cloud is unsupported; no token was requested and no change was sent. | | ||
| | Sign-in prompt names the wrong account | The tenant comes from `az account show`. Run `az account set` / `az login --tenant <id>` to point at the intended tenant, then retry. | |
262 changes: 262 additions & 0 deletions
262
src/Microsoft.Agents.A365.DevTools.Cli/Commands/NetworkCommand.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,262 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using Microsoft.Agents.A365.DevTools.Cli.Constants; | ||
| using Microsoft.Agents.A365.DevTools.Cli.Models; | ||
| using Microsoft.Agents.A365.DevTools.Cli.Services; | ||
| using Microsoft.Extensions.Logging; | ||
| using System.CommandLine; | ||
| using System.CommandLine.Invocation; | ||
|
|
||
| namespace Microsoft.Agents.A365.DevTools.Cli.Commands; | ||
|
|
||
| /// <summary> | ||
| /// Tenant network configuration for Agent 365. | ||
| /// | ||
| /// Global Secure Access is normally set per Power Platform environment, which needs the id of the | ||
| /// environment being configured. Agent 365 does not publish that id, so these subcommands ask the | ||
| /// platform to apply the setting to the environment it resolves for your tenant. | ||
| /// </summary> | ||
| public static class NetworkCommand | ||
| { | ||
| private static readonly TimeSpan DefaultWaitTimeout = TimeSpan.FromMinutes(10); | ||
|
|
||
| /// <summary> | ||
| /// Creates the network command and its gsa subcommand tree. | ||
| /// </summary> | ||
| public static Command CreateCommand( | ||
| ILogger logger, | ||
| IAzureCliService azureCliService, | ||
| IGsaService gsaService, | ||
| IConfirmationProvider confirmationProvider) | ||
| { | ||
| var networkCommand = new Command(CommandNames.Network, "Configure tenant networking for Agent 365"); | ||
|
|
||
| var gsaCommand = new Command( | ||
| "gsa", | ||
| "Turn Global Secure Access on or off for your Agent 365 environment. " + | ||
| "Requires the Global Administrator or Power Platform Administrator role."); | ||
|
|
||
| gsaCommand.AddCommand(CreateGsaSetSubcommand( | ||
| logger, gsaService, azureCliService, confirmationProvider, enabled: true)); | ||
| gsaCommand.AddCommand(CreateGsaSetSubcommand( | ||
| logger, gsaService, azureCliService, confirmationProvider, enabled: false)); | ||
| gsaCommand.AddCommand(CreateGsaStatusSubcommand(logger, gsaService, azureCliService)); | ||
|
|
||
| networkCommand.AddCommand(gsaCommand); | ||
| return networkCommand; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Resolves the Azure account to act as, or logs why it could not and returns null. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Resolved once per invocation and passed to every call that follows. <c>az account show</c> | ||
| /// reads mutable local state, so reading it again for authentication after prompting could | ||
| /// confirm one tenant and change another, and a transient CLI failure between two reads could | ||
| /// fail a command that had already succeeded at resolving the tenant. | ||
| /// </remarks> | ||
| internal static async Task<AzureAccountInfo?> ResolveAccountAsync( | ||
| ILogger logger, | ||
| IAzureCliService azureCliService) | ||
| { | ||
| var account = await azureCliService.GetCurrentAccountAsync(); | ||
| if (account is null || string.IsNullOrWhiteSpace(account.TenantId)) | ||
| { | ||
| logger.LogError("Could not determine your Azure tenant. Run 'az login' and try again."); | ||
| return null; | ||
| } | ||
|
|
||
| return account; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Asks the operator to confirm a change to tenant-wide networking, naming the tenant and the | ||
| /// action so the prompt is answerable without scrolling back. | ||
| /// </summary> | ||
| internal static async Task<bool> ConfirmChangeAsync( | ||
| IConfirmationProvider confirmationProvider, | ||
| bool yes, | ||
| string action, | ||
| string tenantId, | ||
| string? userName = null) | ||
| { | ||
| if (yes) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| var actor = string.IsNullOrWhiteSpace(userName) ? string.Empty : $" as {userName}"; | ||
| return await confirmationProvider.ConfirmAsync( | ||
| $"{action} for tenant {tenantId}{actor}. This changes networking for every Agent 365 agent in the tenant. Continue? (y/N): "); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates the gsa enable or disable subcommand. The two differ only in the value they send | ||
| /// and the words they use, so they share one builder. | ||
| /// </summary> | ||
| private static Command CreateGsaSetSubcommand( | ||
| ILogger logger, | ||
| IGsaService gsaService, | ||
| IAzureCliService azureCliService, | ||
| IConfirmationProvider confirmationProvider, | ||
| bool enabled) | ||
| { | ||
| var verb = enabled ? "enable" : "disable"; | ||
| var command = new Command( | ||
| verb, | ||
| $"Turn Global Secure Access {(enabled ? "on" : "off")} for your Agent 365 environment."); | ||
|
|
||
| var waitOption = new Option<bool>( | ||
| "--wait", | ||
| "Keep polling until the change appears on the environment, instead of returning while " + | ||
| "it is still being applied."); | ||
|
|
||
| var yesOption = new Option<bool>( | ||
| ["--yes", "-y"], | ||
| "Skip the confirmation prompt."); | ||
|
|
||
| var verboseOption = new Option<bool>(["--verbose", "-v"], "Enable verbose logging"); | ||
|
|
||
| command.AddOption(waitOption); | ||
| command.AddOption(yesOption); | ||
| command.AddOption(verboseOption); | ||
|
|
||
| command.SetHandler(async (InvocationContext context) => | ||
| { | ||
| var wait = context.ParseResult.GetValueForOption(waitOption); | ||
| var yes = context.ParseResult.GetValueForOption(yesOption); | ||
| var ct = context.GetCancellationToken(); | ||
|
|
||
| // Resolved once, then used for both the prompt and the call, so the tenant named in | ||
| // the prompt is provably the tenant changed. | ||
| var account = await ResolveAccountAsync(logger, azureCliService); | ||
| if (account == null) | ||
| { | ||
| context.ExitCode = 1; | ||
| return; | ||
| } | ||
|
|
||
| if (!await ConfirmChangeAsync( | ||
| confirmationProvider, yes, $"Turn Global Secure Access {(enabled ? "on" : "off")}", | ||
| account.TenantId, account.User.Name)) | ||
| { | ||
| logger.LogInformation("Cancelled."); | ||
| context.ExitCode = 1; | ||
| return; | ||
| } | ||
|
|
||
| var result = await gsaService.SetAsync(account, enabled, ct); | ||
| context.ExitCode = await ReportGsaAsync(logger, gsaService, account, result, wait, enabled, ct); | ||
| }); | ||
|
|
||
| return command; | ||
| } | ||
|
|
||
| private static Command CreateGsaStatusSubcommand( | ||
| ILogger logger, | ||
| IGsaService gsaService, | ||
| IAzureCliService azureCliService) | ||
| { | ||
| var command = new Command( | ||
| "status", | ||
| "Show whether Global Secure Access is on for your Agent 365 environment."); | ||
|
|
||
| var verboseOption = new Option<bool>(["--verbose", "-v"], "Enable verbose logging"); | ||
| command.AddOption(verboseOption); | ||
|
|
||
| command.SetHandler(async (InvocationContext context) => | ||
| { | ||
| var ct = context.GetCancellationToken(); | ||
|
|
||
| var account = await ResolveAccountAsync(logger, azureCliService); | ||
| if (account == null) | ||
| { | ||
| context.ExitCode = 1; | ||
| return; | ||
| } | ||
|
|
||
| var status = await gsaService.GetStatusAsync(account, ct); | ||
| if (status == null) | ||
| { | ||
| context.ExitCode = 1; | ||
| return; | ||
| } | ||
|
|
||
| LogGsaStatus(logger, status); | ||
| context.ExitCode = 0; | ||
| }); | ||
|
|
||
| return command; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Renders the outcome of a Global Secure Access change, optionally waiting for it to appear | ||
| /// first, and maps it to a process exit code. | ||
| /// </summary> | ||
| internal static async Task<int> ReportGsaAsync( | ||
| ILogger logger, | ||
| IGsaService gsaService, | ||
| AzureAccountInfo account, | ||
| GsaStatusResponse? result, | ||
| bool wait, | ||
| bool enabled, | ||
| CancellationToken cancellationToken) | ||
| { | ||
| if (result == null) | ||
| { | ||
| return 1; | ||
| } | ||
|
|
||
| var expectedStatus = enabled ? "Enabled" : "Disabled"; | ||
|
|
||
| if (wait && result.Pending) | ||
| { | ||
| logger.LogInformation("The change is still being applied. Waiting for it to appear..."); | ||
| result = await gsaService.WaitForStatusAsync(account, expectedStatus, DefaultWaitTimeout, cancellationToken); | ||
|
|
||
| if (result == null) | ||
| { | ||
| logger.LogWarning( | ||
| "The change was accepted, but its current status could not be verified. Check with: a365 network gsa status"); | ||
| return 1; | ||
| } | ||
| } | ||
|
|
||
| if (wait && !string.Equals(result.Status, expectedStatus, StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| LogGsaStatus(logger, result); | ||
| logger.LogWarning( | ||
| "The change was accepted, but Global Secure Access is not {Expected} yet. Check with: a365 network gsa status", | ||
| expectedStatus); | ||
| return 1; | ||
| } | ||
|
|
||
| if (result.Pending) | ||
| { | ||
| logger.LogInformation( | ||
| "Requested: {Expected} (still being applied). Check with: a365 network gsa status", | ||
| expectedStatus); | ||
| } | ||
|
|
||
| LogGsaStatus(logger, result, result.Pending ? "Current" : "Global Secure Access"); | ||
| return 0; | ||
| } | ||
|
|
||
| private static void LogGsaStatus(ILogger logger, GsaStatusResponse status, string label = "Global Secure Access") | ||
| { | ||
| logger.LogInformation("{Label}: {Status}", label, status.Status ?? "Unknown"); | ||
|
|
||
| if (string.Equals(status.Status, "NotConfigured", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| // Worth spelling out: a tenant that has never set this is not the same as one that | ||
| // turned it off, and the distinction changes what an admin should do next. | ||
| logger.LogInformation("This tenant has never set Global Secure Access, so no value is stored."); | ||
| } | ||
|
|
||
| if (!string.IsNullOrWhiteSpace(status.Reason)) | ||
| { | ||
| logger.LogWarning("Reason: {Reason}", status.Reason); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.