Skip to content

Provision the CLI service principal via Agent 365 on first tenant use - #481

Open
Krishnadheeraj (DheerajPannala) wants to merge 1 commit into
mainfrom
kpannala/a365-cli-sp-provisioning
Open

Provision the CLI service principal via Agent 365 on first tenant use#481
Krishnadheeraj (DheerajPannala) wants to merge 1 commit into
mainfrom
kpannala/a365-cli-sp-provisioning

Conversation

@DheerajPannala

@DheerajPannala Krishnadheeraj (DheerajPannala) commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Draft. Paired with a corresponding service-side change; this PR cannot function until that
ships
, so please treat it as design review rather than merge-ready.

Problem

In a newly onboarded tenant, the CLI's service principal does not yet exist, so a365 setup
fails at sign-in until an administrator creates it by hand. That manual step is easy to miss and
the resulting error is not self-explanatory.

The CLI cannot create it itself: service principals are provisioned just-in-time only for
applications that authenticate with a credential, and the CLI is a public client by design.

Change

ServicePrincipalProvisioningService asks the Agent 365 service to perform the provisioning on
the CLI's behalf, and the CLI calls it the first time it operates against a tenant.

The call is made from BootstrapConfigResolver.ResolveAsync, the shared entry point behind the
setup, cleanup and publish flows, so one insertion point covers every command.

Behaviour:

  • Runs at most once per tenant per process, deduplicated through a ConcurrentDictionary of
    in-flight tasks, so concurrent callers share a single attempt
  • Never throws. Failures are logged at debug level and the command continues and reports its
    own errors; provisioning is a convenience, not a precondition
  • Skipped when no tenant id is available or it is not a well-formed GUID
  • Opt-out via A365_DISABLE_SP_PROVISIONING=true, for CI and restricted environments

Token used — feedback welcome

The request carries a delegated user token with audience https://api.powerplatform.com.

The client id falls through to AuthenticationConstants.PowershellClientId, the existing default
in AuthenticationService. That is deliberate: the CLI cannot authenticate as itself here,
because its service principal not yet existing is precisely the condition being resolved. Azure
PowerShell is present in every tenant, so it is usable before the CLI is.

Two review questions:

  1. Should the client id be passed explicitly with a comment rather than inherited from the
    default? It is load-bearing here, and a silent default makes that invisible. I lean yes.
  2. Can anyone confirm Azure PowerShell is consented for the Power Platform API resource in a
    clean tenant? If not this returns AADSTS65001, and the CLI would need a different actor.

For context, this matches existing behaviour: TeamsGraphBackendConfigurator and other callers
also rely on the same default.

Testing

Suite Result
New ServicePrincipalProvisioningServiceTests 12 passed
Full CLI suite 1933 passed, 12 skipped

Covered: status mapping, request shape (route, api-version, bearer scheme), once-per-tenant
deduplication, invalid tenant ids skipping without any HTTP call, 403 and transport failures
degrading without throwing, and the opt-out switch.

Not covered: end-to-end. The service-side dependency is not deployed, so no part of this has
run against a live tenant. That is the main risk in this change.

Notes for reviewers

  • Tenant id is GUID-validated before it is interpolated into a request URL
  • HttpClient and HttpResponseMessage are disposed via using throughout
  • Endpoint overrides follow the existing convention (A365_PROVISIONING_ENDPOINT,
    A365_PROVISIONING_ENDPOINT_{ENV})
  • CHANGELOG entry added under [Unreleased]
  • The hook is placed after config resolution so it never runs before a tenant is known

@github-actions

Copy link
Copy Markdown

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 28, 2026
In a newly onboarded tenant the CLI service principal does not exist yet,
so setup fails at sign-in until an administrator creates it by hand. The
CLI cannot create it itself: service principals are provisioned just in
time only for applications that authenticate with a credential, and the
CLI is a public client.

The CLI now asks the Agent 365 service to provision it, once per tenant
per process, from BootstrapConfigResolver.ResolveAsync - the shared entry
point behind the setup, cleanup and publish flows. The call is best
effort: it never throws, is deduplicated per tenant, and can be disabled
with A365_DISABLE_SP_PROVISIONING=true.

Requires a corresponding service-side change before it has any effect.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f8ba1b67-3303-43fd-935f-81941a9e9ef3
@DheerajPannala
Krishnadheeraj (DheerajPannala) force-pushed the kpannala/a365-cli-sp-provisioning branch from 46d0d82 to 669b6b5 Compare July 28, 2026 20:48
@DheerajPannala
Krishnadheeraj (DheerajPannala) marked this pull request as ready for review August 31, 2026 19:32
Copilot AI lite review requested due to automatic review settings August 31, 2026 19:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a best-effort, once-per-tenant-per-process bootstrap hook that asks the Agent 365 service (via the Power Platform gateway) to provision the CLI’s service principal on first use in a tenant, reducing manual admin setup for newly onboarded tenants.

Changes:

  • Introduces ServicePrincipalProvisioningService to POST to a tenant-scoped provisioning route using a delegated Power Platform token, with opt-out via A365_DISABLE_SP_PROVISIONING.
  • Hooks provisioning into BootstrapConfigResolver.ResolveAsync so setup/cleanup/publish flows all benefit from the shared bootstrap entry point.
  • Adds constants for the provisioning base URL and route format, plus new unit tests and a [Unreleased] CHANGELOG entry.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/ServicePrincipalProvisioningServiceTests.cs Adds unit coverage for status mapping, request shape, dedupe, skipping, and opt-out.
src/Microsoft.Agents.A365.DevTools.Cli/Services/ServicePrincipalProvisioningService.cs New service implementing best-effort provisioning call + in-flight dedupe.
src/Microsoft.Agents.A365.DevTools.Cli/Services/BootstrapConfigResolver.cs Calls provisioning after config resolution when a tenant is known.
src/Microsoft.Agents.A365.DevTools.Cli/Program.cs Registers the provisioning service in DI.
src/Microsoft.Agents.A365.DevTools.Cli/Constants/ConfigConstants.cs Adds provisioning base URL + route constants and endpoint override helper.
CHANGELOG.md Documents the new behavior and opt-out env var.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +137 to +140
var key = parsedTenantId.ToString();

// One attempt per tenant per process; concurrent callers share the same attempt.
return _inFlight.GetOrAdd(key, _ => ProvisionAsync(parsedTenantId, userId, ct));
Comment on lines +258 to +273
var status = payload?.Status switch
{
"Provisioned" => ServicePrincipalProvisioningStatus.Provisioned,
"AlreadyProvisioned" => ServicePrincipalProvisioningStatus.AlreadyProvisioned,
"Disabled" => ServicePrincipalProvisioningStatus.Skipped,
"Failed" => ServicePrincipalProvisioningStatus.Failed,
_ => ServicePrincipalProvisioningStatus.Provisioned,
};

_logger.LogDebug(
"Agent 365 CLI service principal provisioning for tenant {TenantId} returned {Status}.",
tenantId,
status);

return new ServicePrincipalProvisioningResult(
status, payload?.ServicePrincipalObjectId, payload?.Detail);
Comment on lines +167 to +171
var authToken = await _authService.GetAccessTokenAsync(
PowerPlatformConstants.PowerPlatformApiIdentifierUri,
tenantId.ToString(),
userId: userId,
ct: ct);
Comment thread CHANGELOG.md
**Option B — CLI** (`a365 setup admin`) has been removed in this release. Use Option A above, or copy the PowerShell instructions printed in the `a365 setup all` summary output.

### Added
- The CLI now asks the Agent 365 service to register its own service principal the first time it runs against a tenant, removing a manual admin step that previously caused sign-in to fail in newly onboarded tenants. Set `A365_DISABLE_SP_PROVISIONING=true` to opt out.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants