| File: EntraIdApiAccess.cs | Web Access |
| Project: src\src\Aspire.Hosting.Azure.EntraId\Aspire.Hosting.Azure.EntraId.csproj (Aspire.Hosting.Azure.EntraId) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.ApplicationModel; namespace Aspire.Hosting.Azure; /// <summary> /// A client application that an API registration pre-authorizes for some of its scopes. /// </summary> internal sealed record EntraIdPreAuthorizedClient(EntraIdApplicationResource Client, IReadOnlyList<EntraIdScope> Scopes); /// <summary> /// The permissions that an application declares, and optionally has consented, for one API. /// </summary> /// <remarks> /// The API is either an application registration modeled in the AppHost or a well-known API such as Microsoft Graph. /// Declaring a permission only lists it in the application's required permissions. Consent is tracked separately, /// because it is what lets the application use the permission without prompting anyone. /// </remarks> internal sealed class EntraIdApiAccess { private EntraIdApiAccess(EntraIdApplicationResource? application, EntraIdApi? api) { Application = application; Api = api; } public static EntraIdApiAccess ForApplication(EntraIdApplicationResource application) => new(application, null); public static EntraIdApiAccess ForApi(EntraIdApi api) => new(null, api); public EntraIdApplicationResource? Application { get; } public EntraIdApi? Api { get; } public List<EntraIdResolvedPermission> Declared { get; } = []; public List<EntraIdResolvedPermission> Consented { get; } = []; public string DisplayName => Application?.Name ?? Api!.DisplayName; public bool IsFor(EntraIdApplicationResource application) => ReferenceEquals(Application, application); public bool IsFor(EntraIdApi api) => Api is not null && string.Equals(Api.ApplicationId, api.ApplicationId, StringComparison.OrdinalIgnoreCase); // The names of the API's entries under Microsoft.Identity.Web's DownstreamApis section. Each is bound as one configuration key. // For an API registration in the AppHost these are the resources that reference it, as with service discovery, because // those are the resources that the caller sends requests to and that name their API in code, such as the host of // an HttpClient's BaseAddress. They are read when the environment is resolved, so references added later are included. private IEnumerable<string> GetConfigurationNames() { if (Application is { } application) { // A registration that nothing references has no resource to name the entry after. return application.Consumers.Count > 0 ? application.Consumers.Select(static c => c.Name).Distinct(StringComparer.OrdinalIgnoreCase) : [EnvironmentVariableNameEncoder.EncodeConnectionStringName(application.Name)]; } return [new string([.. Api!.DisplayName.Where(char.IsAsciiLetterOrDigit)]) is { Length: > 0 } displayName ? displayName : EnvironmentVariableNameEncoder.EncodeConnectionStringName(Api.ApplicationId)]; } /// <summary> /// Describes the token requests that the caller makes for this API, as Microsoft.Identity.Web's <c>DownstreamApis</c> entries. /// </summary> /// <remarks> /// A token request covers one resource, and the delegated and application flows differ, so they are separate entries: /// delegated permissions are the scopes of <c>{name}</c> (acting for a signed-in user), and application permissions /// are the <c>.default</c> scope of <c>{name}AppOnly</c> (acting as the application itself). /// </remarks> public IEnumerable<EntraIdDownstreamApi> GetDownstreamApis() { var delegatedScopes = Declared .Where(p => p.Type == EntraIdPermissionType.Delegated && !(Api?.IsOpenIdScope(p.Name) ?? false)) .Select(p => CreateScopeValue(p.Name)) .ToArray(); var hasApplicationPermissions = Declared.Any(p => p.Type == EntraIdPermissionType.Application); foreach (var name in GetConfigurationNames()) { if (delegatedScopes.Length > 0) { yield return new EntraIdDownstreamApi(name, Api?.BaseUrl, RequestAppToken: false, delegatedScopes); } if (hasApplicationPermissions) { yield return new EntraIdDownstreamApi($"{name}AppOnly", Api?.BaseUrl, RequestAppToken: true, [CreateScopeValue(".default")]); } } } private object CreateScopeValue(string scope) => Application is { } application ? application.CreateScopeValue(scope) : EntraIdApi.CreateScope(Api!.ResourceUri, scope); } /// <summary> /// One entry of Microsoft.Identity.Web's <c>DownstreamApis</c> section that Aspire configures for a caller. /// </summary> /// <param name="Name">The entry's name.</param> /// <param name="BaseUrl">The API's root URL when it is the same for every caller; otherwise <see langword="null"/>.</param> /// <param name="RequestAppToken">Whether the call is made as the application, not on behalf of a user.</param> /// <param name="Scopes">The scopes to request: each a string or a <see cref="ApplicationModel.ReferenceExpression"/>.</param> internal sealed record EntraIdDownstreamApi(string Name, string? BaseUrl, bool RequestAppToken, IReadOnlyList<object> Scopes);