Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ private static async Task EnsureMcpInheritablePermissionsAsync(
var requiredPermissions = new[] { "AgentIdentityBlueprint.UpdateAuthProperties.All", "Application.ReadWrite.All" };

var (ok, alreadyExists, err) = await blueprintService.SetInheritablePermissionsAsync(
config.TenantId, config.AgentBlueprintId, resourceAppId, scopes, requiredScopes: requiredPermissions, ct);
config.TenantId, config.AgentBlueprintId, resourceAppId, scopes, requiredPermissions, ct);

if (!ok && !alreadyExists)
{
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -332,17 +332,17 @@ public static async Task<bool> ValidateAzureCliAuthenticationAsync(
var accountCheck = await executor.ExecuteAsync("az", "account show", captureOutput: true, suppressErrorLogging: true, cancellationToken: cancellationToken);
if (!accountCheck.Success)
{
logger.LogInformation("Azure CLI not authenticated. Initiating login with management scope...");
logger.LogInformation("A browser window will open for authentication.");
var loginResult = await executor.ExecuteAsync("az", $"login --tenant {tenantId}", cancellationToken: cancellationToken);
logger.LogInformation("Azure CLI not authenticated. Initiating device code login...");
logger.LogInformation("Please follow the device code instructions in your terminal.");

var loginResult = await executor.ExecuteAsync("az", $"login --tenant {tenantId} --use-device-code --allow-no-subscriptions", cancellationToken: cancellationToken);

if (!loginResult.Success)
{
logger.LogError("Azure CLI login failed. Please run manually: az login --scope https://management.core.windows.net//.default");
logger.LogError("Azure CLI login failed. Please run manually: az login --tenant {TenantId} --use-device-code --scope https://management.core.windows.net//.default", tenantId);
return false;
}

logger.LogInformation("Azure CLI login successful!");
await Task.Delay(2000, cancellationToken);
}
Expand All @@ -362,17 +362,17 @@ public static async Task<bool> ValidateAzureCliAuthenticationAsync(

if (!tokenCheck.Success)
{
logger.LogWarning("Unable to acquire management scope token. Attempting re-authentication...");
logger.LogInformation("A browser window will open for authentication.");
var loginResult = await executor.ExecuteAsync("az", $"login --tenant {tenantId}", cancellationToken: cancellationToken);
logger.LogWarning("Unable to acquire management scope token. Attempting device code re-authentication...");
logger.LogInformation("Please follow the device code instructions in your terminal.");

var loginResult = await executor.ExecuteAsync("az", $"login --tenant {tenantId} --use-device-code --allow-no-subscriptions", cancellationToken: cancellationToken);

if (!loginResult.Success)
{
logger.LogError("Azure CLI login with management scope failed. Please run manually: az login --scope https://management.core.windows.net//.default");
logger.LogError("Azure CLI login with management scope failed. Please run manually: az login --tenant {TenantId} --use-device-code --scope https://management.core.windows.net//.default", tenantId);
return false;
}

logger.LogInformation("Azure CLI re-authentication successful!");
await Task.Delay(2000, cancellationToken);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,20 +210,20 @@ public static async Task EnsureResourcePermissionsAsync(
throw new SetupValidationException("AgentBlueprintId (appId) is required.");

// Use delegated token provider for *all* permission operations to avoid bouncing between Azure CLI auth and Microsoft Graph PowerShell auth.
var permissionGrantScopes = AuthenticationConstants.RequiredPermissionGrantScopes;
var permissionGrantScopes = AuthenticationConstants.PermissionGrantAuthScopes;

// Pre-warm the delegated token once
var user = await graph.GraphGetAsync(
var warmup = await graph.GraphGetAsync(
config.TenantId,
"/v1.0/me?$select=id",
ct,
scopes: permissionGrantScopes);
if (user == null)

if (warmup == null)
{
throw new SetupValidationException(
"Failed to authenticate to Microsoft Graph with delegated permissions. " +
"Please sign in when prompted and ensure your account has the required roles and permission scopes.");
"Failed to authenticate to Microsoft Graph with delegated permissions required for permission grants. " +
"Please sign in when prompted and ensure your account has the required roles and the custom client app has admin-consented scopes.");
}

var blueprintSpObjectId = await graph.LookupServicePrincipalByAppIdAsync(config.TenantId, config.AgentBlueprintId, ct, permissionGrantScopes);
Expand Down Expand Up @@ -287,7 +287,7 @@ public static async Task EnsureResourcePermissionsAsync(
var requiredPermissions = new[] { "AgentIdentityBlueprint.UpdateAuthProperties.All", "Application.ReadWrite.All" };

var (ok, alreadyExists, err) = await blueprintService.SetInheritablePermissionsAsync(
config.TenantId, config.AgentBlueprintId, resourceAppId, scopes, requiredScopes: requiredPermissions, ct);
config.TenantId, config.AgentBlueprintId, resourceAppId, scopes, requiredPermissions, ct);

if (!ok && !alreadyExists)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,24 @@ public static class AuthenticationConstants
/// These scopes enable the service principals to operate correctly with the necessary permissions.
/// All scopes require admin consent.
/// </summary>
public static readonly string[] RequiredPermissionGrantScopes = new[]
public static readonly string[] PermissionGrantAuthScopes = new[]
{
"User.Read",
"Application.ReadWrite.All",
"DelegatedPermissionGrant.ReadWrite.All"
};

/// <summary>
/// Required scopes for agent blueprint operations using delegated authentication.
/// These scopes enable creating, updating, and deleting agent blueprints.
/// All scopes require admin consent.
/// </summary>
public static readonly string[] AgentBlueprintAuthScopes = new[]
{
"User.Read",
"AgentIdentityBlueprint.ReadWrite.All"
};

/// <summary>
/// Environment variable name for bearer token used in local development.
/// This token is stored in .env files (Python/Node.js) or launchSettings.json (.NET)
Expand Down
67 changes: 48 additions & 19 deletions src/Microsoft.Agents.A365.DevTools.Cli/Helpers/PublishHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,14 @@ private static async Task<bool> CheckMosPrerequisitesAsync(
ILogger logger,
CancellationToken ct)
{
var authScopes = AuthenticationConstants.PermissionGrantAuthScopes;

// Check 1: Verify all required service principals exist
var firstPartyClientSpId = await graph.LookupServicePrincipalByAppIdAsync(config.TenantId,
MosConstants.TpsAppServicesClientAppId, ct);
var firstPartyClientSpId = await graph.LookupServicePrincipalByAppIdAsync(
config.TenantId,
MosConstants.TpsAppServicesClientAppId,
ct,
authScopes);
if (string.IsNullOrWhiteSpace(firstPartyClientSpId))
{
logger.LogDebug("Service principal for {ConstantName} ({AppId}) not found - configuration needed",
Expand All @@ -39,7 +44,7 @@ private static async Task<bool> CheckMosPrerequisitesAsync(

foreach (var resourceAppId in MosConstants.AllResourceAppIds)
{
var spId = await graph.LookupServicePrincipalByAppIdAsync(config.TenantId, resourceAppId, ct);
var spId = await graph.LookupServicePrincipalByAppIdAsync(config.TenantId, resourceAppId, ct, authScopes);
if (string.IsNullOrWhiteSpace(spId))
{
logger.LogDebug("Service principal for {ResourceAppId} not found - configuration needed", resourceAppId);
Expand Down Expand Up @@ -109,9 +114,11 @@ private static async Task<bool> CheckMosPrerequisitesAsync(
}

// Check if OAuth2 permission grant exists
var grantDoc = await graph.GraphGetAsync(config.TenantId,
var grantDoc = await graph.GraphGetAsync(
config.TenantId,
$"/v1.0/oauth2PermissionGrants?$filter=clientId eq '{firstPartyClientSpId}' and resourceId eq '{resourceSpId}'",
ct);
ct,
authScopes);

if (grantDoc == null || !grantDoc.RootElement.TryGetProperty("value", out var grants) || grants.GetArrayLength() == 0)
{
Expand Down Expand Up @@ -155,18 +162,26 @@ private static async Task EnsureMosServicePrincipalsAsync(
ILogger logger,
CancellationToken ct)
{
var authScopes = AuthenticationConstants.PermissionGrantAuthScopes;

// Check 1: First-party client app service principal
var firstPartySpId = await graph.LookupServicePrincipalByAppIdAsync(config.TenantId,
MosConstants.TpsAppServicesClientAppId, ct);
var firstPartySpId = await graph.LookupServicePrincipalByAppIdAsync(
config.TenantId,
MosConstants.TpsAppServicesClientAppId,
ct,
authScopes);

if (string.IsNullOrWhiteSpace(firstPartySpId))
{
logger.LogInformation("Creating service principal for Microsoft first-party client app...");

try
{
firstPartySpId = await graph.EnsureServicePrincipalForAppIdAsync(config.TenantId,
MosConstants.TpsAppServicesClientAppId, ct);
firstPartySpId = await graph.EnsureServicePrincipalForAppIdAsync(
config.TenantId,
MosConstants.TpsAppServicesClientAppId,
ct,
authScopes);

if (string.IsNullOrWhiteSpace(firstPartySpId))
{
Expand Down Expand Up @@ -201,7 +216,7 @@ private static async Task EnsureMosServicePrincipalsAsync(
var missingResourceApps = new List<string>();
foreach (var resourceAppId in MosConstants.AllResourceAppIds)
{
var spId = await graph.LookupServicePrincipalByAppIdAsync(config.TenantId, resourceAppId, ct);
var spId = await graph.LookupServicePrincipalByAppIdAsync(config.TenantId, resourceAppId, ct, authScopes);
if (string.IsNullOrWhiteSpace(spId))
{
missingResourceApps.Add(resourceAppId);
Expand All @@ -216,7 +231,7 @@ private static async Task EnsureMosServicePrincipalsAsync(
{
try
{
var spId = await graph.EnsureServicePrincipalForAppIdAsync(config.TenantId, resourceAppId, ct);
var spId = await graph.EnsureServicePrincipalForAppIdAsync(config.TenantId, resourceAppId, ct, authScopes);

if (string.IsNullOrWhiteSpace(spId))
{
Expand Down Expand Up @@ -260,6 +275,8 @@ private static async Task EnsureMosPermissionsConfiguredAsync(
ILogger logger,
CancellationToken ct)
{
var authScopes = AuthenticationConstants.PermissionGrantAuthScopes;

if (!app.TryGetProperty("id", out var appObjectIdElement))
{
throw new SetupValidationException($"Application {config.ClientAppId} missing id property");
Expand Down Expand Up @@ -382,7 +399,7 @@ private static async Task EnsureMosPermissionsConfiguredAsync(
logger.LogDebug("Updating application {AppObjectId} with {Count} resource access entries",
appObjectId, updatedResourceAccess.Count);

var updated = await graph.GraphPatchAsync(config.TenantId, $"/v1.0/applications/{appObjectId}", patchPayload, ct);
var updated = await graph.GraphPatchAsync(config.TenantId, $"/v1.0/applications/{appObjectId}", patchPayload, ct, authScopes);
if (!updated)
{
throw new SetupValidationException("Failed to update application with MOS API permissions.");
Expand Down Expand Up @@ -414,6 +431,8 @@ public static async Task<bool> EnsureMosPrerequisitesAsync(
ILogger logger,
CancellationToken ct = default)
{
var authScopes = AuthenticationConstants.PermissionGrantAuthScopes;

if (string.IsNullOrWhiteSpace(config.ClientAppId))
{
logger.LogError("Custom client app ID not found in configuration. Run 'a365 config init' first.");
Expand All @@ -422,8 +441,11 @@ public static async Task<bool> EnsureMosPrerequisitesAsync(

// Load custom client app
logger.LogDebug("Checking MOS prerequisites for custom client app {ClientAppId}", config.ClientAppId);
var appDoc = await graph.GraphGetAsync(config.TenantId,
$"/v1.0/applications?$filter=appId eq '{config.ClientAppId}'&$select=id,requiredResourceAccess", ct);
var appDoc = await graph.GraphGetAsync(
config.TenantId,
$"/v1.0/applications?$filter=appId eq '{config.ClientAppId}'&$select=id,requiredResourceAccess",
ct,
authScopes);

if (appDoc == null || !appDoc.RootElement.TryGetProperty("value", out var appsArray) || appsArray.GetArrayLength() == 0)
{
Expand Down Expand Up @@ -467,9 +489,14 @@ private static async Task EnsureMosAdminConsentAsync(
ILogger logger,
CancellationToken ct)
{
var authScopes = AuthenticationConstants.PermissionGrantAuthScopes;

// Look up the first-party client app's service principal
var clientSpObjectId = await graph.LookupServicePrincipalByAppIdAsync(config.TenantId,
MosConstants.TpsAppServicesClientAppId, ct);
var clientSpObjectId = await graph.LookupServicePrincipalByAppIdAsync(
config.TenantId,
MosConstants.TpsAppServicesClientAppId,
ct,
authScopes);

if (string.IsNullOrWhiteSpace(clientSpObjectId))
{
Expand All @@ -487,17 +514,19 @@ private static async Task EnsureMosAdminConsentAsync(
// Check which resources need consent
foreach (var (resourceAppId, scopeName) in mosResourceScopes)
{
var resourceSpObjectId = await graph.LookupServicePrincipalByAppIdAsync(config.TenantId, resourceAppId, ct);
var resourceSpObjectId = await graph.LookupServicePrincipalByAppIdAsync(config.TenantId, resourceAppId, ct, authScopes);
if (string.IsNullOrWhiteSpace(resourceSpObjectId))
{
logger.LogWarning("Service principal not found for MOS resource app {ResourceAppId} - skipping consent", resourceAppId);
continue;
}

// Check if consent already exists
var grantDoc = await graph.GraphGetAsync(config.TenantId,
var grantDoc = await graph.GraphGetAsync(
config.TenantId,
$"/v1.0/oauth2PermissionGrants?$filter=clientId eq '{clientSpObjectId}' and resourceId eq '{resourceSpObjectId}'",
ct);
ct,
authScopes);

var hasConsent = false;
if (grantDoc != null && grantDoc.RootElement.TryGetProperty("value", out var grants) && grants.GetArrayLength() > 0)
Expand Down
Empty file.
Loading
Loading