diff --git a/src/Bravo.csproj b/src/Bravo.csproj index c2e31494..0ccd941d 100644 --- a/src/Bravo.csproj +++ b/src/Bravo.csproj @@ -12,6 +12,7 @@ SQLBI Corporation Sqlbi.$(MSBuildProjectName.Replace(" ", "_")) enable + 14.0 True $(NoWarn);1591 true diff --git a/src/Controllers/AuthenticationController.cs b/src/Controllers/AuthenticationController.cs index 08ffcf72..3cc6ac6a 100644 --- a/src/Controllers/AuthenticationController.cs +++ b/src/Controllers/AuthenticationController.cs @@ -2,17 +2,9 @@ { using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.Configuration; - using Sqlbi.Bravo.Infrastructure.Models.PBICloud; using Sqlbi.Bravo.Infrastructure.Services.PowerBI; - using Sqlbi.Bravo.Models; + using Sqlbi.Bravo.Models.Authentication; using Sqlbi.Bravo.Services; - using System.Collections.Generic; - using System.Linq; - using System.Net.Mime; - using System.Threading; - using System.Threading.Tasks; /// /// Authentication controller @@ -21,30 +13,34 @@ [Route("auth/[action]")] [ApiController] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] - public class AuthenticationController : ControllerBase + public sealed class AuthenticationController( + IPBICloudAuthenticationService pbicloudAuthenticationService, + IPBICloudService pbicloudService, + IAuthenticationService authenticationService) : ControllerBase { - private readonly IAuthenticationService _authenticationService; - private readonly IPBICloudService _pbicloudService; - - public AuthenticationController(IAuthenticationService authenticationService, IPBICloudService pbicloudService) - { - _authenticationService = authenticationService; - _pbicloudService = pbicloudService; - } + private readonly IAuthenticationService _authenticationService = authenticationService; + private readonly IPBICloudAuthenticationService _pbicloudAuthenticationService = pbicloudAuthenticationService; + private readonly IPBICloudService _pbicloudService = pbicloudService; /// - /// TODO + /// Returns the list of available PowerBI cloud environments for the specified email account. /// /// Status200OK - Success [HttpGet] - [ActionName("powerbi/GetEnvironments")] + [ActionName("GetEnvironments")] [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetEnvironmentsResponse))] [ProducesDefaultResponseType] - public async Task GetPBICloudEnvironmentsAsync(string userPrincipalName, CancellationToken cancellationToken) + public async Task GetEnvironmentsAsync( + [FromQuery] GetEnvironmentsRequest request, + CancellationToken cancellationToken) { - var environments = await _authenticationService.GetPBICloudEnvironmentsAsync(userPrincipalName, cancellationToken); - return Ok(environments); + var environments = await _pbicloudAuthenticationService.GetEnvironmentsAsync( + request.Email, + cancellationToken); + + var response = new GetEnvironmentsResponse(environments); + return Ok(response); } /// @@ -52,14 +48,21 @@ public async Task GetPBICloudEnvironmentsAsync(string userPrincip /// /// Status200OK - Success [HttpPost] - [ActionName("powerbi/SignIn")] + [ActionName("SignIn")] [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IBravoAccount))] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(SignInResponse))] [ProducesDefaultResponseType] - public async Task PBICloudSignInAsync(PBICloudAuthenticationRequest request, CancellationToken cancellationToken) + public async Task SignInAsync( + SignInRequest request, + CancellationToken cancellationToken) { - await _authenticationService.PBICloudSignInAsync(request.UserPrincipalName!, request.Environment!, cancellationToken); - return Ok(_authenticationService.PBICloudAuthentication.Account); + await _authenticationService.PBICloudSignInAsync( + request.Email, + request.Environment.ToModel(), + cancellationToken); + + var response = new SignInResponse(_authenticationService.PBICloudAuthentication.Account); + return Ok(response); } /// @@ -67,10 +70,10 @@ public async Task PBICloudSignInAsync(PBICloudAuthenticationReque /// /// Status200OK - Success [HttpGet] - [ActionName("powerbi/SignOut")] + [ActionName("SignOut")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesDefaultResponseType] - public async Task PBICloudSignOutAsync(CancellationToken cancellationToken) + public async Task SignOutAsync(CancellationToken cancellationToken) { await _authenticationService.PBICloudSignOutAsync(cancellationToken); return Ok(); @@ -89,7 +92,7 @@ public async Task PBICloudSignOutAsync(CancellationToken cancella [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesDefaultResponseType] - public async Task GetPBICloudAccountAvatarAsync(CancellationToken cancellationToken) + public async Task GetUserAvatarAsync(CancellationToken cancellationToken) { if (await _authenticationService.IsPBICloudSignInRequiredAsync(cancellationToken)) return Unauthorized(); diff --git a/src/Infrastructure/AppEnvironment.cs b/src/Infrastructure/AppEnvironment.cs index fef4032b..a08d80ae 100644 --- a/src/Infrastructure/AppEnvironment.cs +++ b/src/Infrastructure/AppEnvironment.cs @@ -55,7 +55,6 @@ internal static class AppEnvironment // NBSP char instead of whitespace - Latvian/lv "\u00A0\u2014 Power BI Desktop", }; - public static readonly TimeSpan MSALSignInTimeout = TimeSpan.FromMinutes(5); public static readonly Color ThemeColorDark = ColorTranslator.FromHtml("#202020"); public static readonly Color ThemeColorLight = ColorTranslator.FromHtml("#F3F3F3"); public static readonly DaxLineBreakStyle FormatDaxLineBreakDefault = DaxLineBreakStyle.InitialLineBreak; diff --git a/src/Infrastructure/Contracts/PBICloud/CloudEnvironmentClientContract.cs b/src/Infrastructure/Contracts/PBICloud/CloudEnvironmentClientContract.cs new file mode 100644 index 00000000..1c019c4b --- /dev/null +++ b/src/Infrastructure/Contracts/PBICloud/CloudEnvironmentClientContract.cs @@ -0,0 +1,23 @@ +namespace Sqlbi.Bravo.Infrastructure.Contracts.PBICloud +{ + using System.Text.Json.Serialization; + + [DebuggerDisplay("{Name}")] + internal sealed class CloudEnvironmentClientContract + { + [JsonPropertyName("name")] + public string Name { get; set; } = null!; + + [JsonPropertyName("appId")] + public string AppId { get; set; } = null!; + + [JsonPropertyName("redirectUri")] + public string RedirectUri { get; set; } = null!; + } + + internal static class CloudEnvironmentClientContractExtension + { + public static bool IsPowerBIDesktop(this CloudEnvironmentClientContract client) + => client.Name.Equals("powerbi-desktop", StringComparison.Ordinal); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Contracts/PBICloud/CloudEnvironmentContract.cs b/src/Infrastructure/Contracts/PBICloud/CloudEnvironmentContract.cs new file mode 100644 index 00000000..35f21a7a --- /dev/null +++ b/src/Infrastructure/Contracts/PBICloud/CloudEnvironmentContract.cs @@ -0,0 +1,42 @@ +namespace Sqlbi.Bravo.Infrastructure.Contracts.PBICloud +{ + using System.Text.Json.Serialization; + + [DebuggerDisplay("{CloudName}")] + internal sealed class CloudEnvironmentContract + { + [JsonPropertyName("cloudName")] + public string CloudName { get; set; } = null!; + + [JsonPropertyName("clients")] + public CloudEnvironmentClientContract[] Clients { get; set; } = null!; + + [JsonPropertyName("services")] + public CloudEnvironmentServiceContract[] Services { get; set; } = null!; + } + + internal static class CloudEnvironmentContractExtension + { + public static string GetDescription(this CloudEnvironmentContract environment) => environment.CloudName switch + { + "GlobalCloud" => "Power BI", + "ChinaCloud" => "Power BI operated by 21Vianet in China", + "USGovCloud" => "Power BI for US Government", // gcc + "USGovDoDL4Cloud" => "Power BI for US Government (L4)", // gcc_high + "USGovDoDL5Cloud" => "Power BI for US Government (L5)", // gcc_dod + _ => environment.CloudName, + }; + + public static bool IsMicrosoftInternalCloud(this CloudEnvironmentContract environment) + => s_microsoftInternalClouds.Contains(environment.CloudName); + + private readonly static HashSet s_microsoftInternalClouds = new(StringComparer.OrdinalIgnoreCase) + { + "OneBox", + "DAILY", + "Int3", + "PpeCloud", // edog + "DXT" + }; + } +} diff --git a/src/Infrastructure/Contracts/PBICloud/CloudEnvironmentResponseContract.cs b/src/Infrastructure/Contracts/PBICloud/CloudEnvironmentResponseContract.cs new file mode 100644 index 00000000..8a15f433 --- /dev/null +++ b/src/Infrastructure/Contracts/PBICloud/CloudEnvironmentResponseContract.cs @@ -0,0 +1,13 @@ +namespace Sqlbi.Bravo.Infrastructure.Contracts.PBICloud +{ + using System.Text.Json.Serialization; + + // Sample response from the discover API: + // Invoke-RestMethod -Method POST -Uri "https://api.powerbi.com/powerbi/globalservice/v202003/environments/discover?client=powerbi-msolap" | ConvertTo-Json -Depth 10 + + internal sealed class CloudEnvironmentResponseContract + { + [JsonPropertyName("environments")] + public CloudEnvironmentContract[] Environments { get; set; } = null!; + } +} diff --git a/src/Infrastructure/Contracts/PBICloud/CloudEnvironmentServiceContract.cs b/src/Infrastructure/Contracts/PBICloud/CloudEnvironmentServiceContract.cs new file mode 100644 index 00000000..e92f5852 --- /dev/null +++ b/src/Infrastructure/Contracts/PBICloud/CloudEnvironmentServiceContract.cs @@ -0,0 +1,29 @@ +namespace Sqlbi.Bravo.Infrastructure.Contracts.PBICloud +{ + using System.Text.Json.Serialization; + + [DebuggerDisplay("{Name}")] + internal sealed class CloudEnvironmentServiceContract + { + [JsonPropertyName("name")] + public string Name { get; set; } = null!; + + [JsonPropertyName("endpoint")] + public string Endpoint { get; set; } = null!; + + [JsonPropertyName("resourceId")] + public string ResourceId { get; set; } = null!; + + //[JsonPropertyName("allowedDomains")] + //public string[] AllowedDomains { get; set; } = null!; + } + + internal static class CloudEnvironmentServiceContractExtension + { + public static bool IsAad(this CloudEnvironmentServiceContract service) + => service.Name.Equals("aad", StringComparison.Ordinal); + + public static bool IsPowerBIBackend(this CloudEnvironmentServiceContract service) + => service.Name.Equals("powerbi-backend", StringComparison.Ordinal); + } +} diff --git a/src/Infrastructure/Contracts/PBICloud/GlobalService.cs b/src/Infrastructure/Contracts/PBICloud/GlobalService.cs deleted file mode 100644 index 9a0a7145..00000000 --- a/src/Infrastructure/Contracts/PBICloud/GlobalService.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Sqlbi.Bravo.Infrastructure.Contracts.PBICloud -{ - using System.Collections.Generic; - using System.Text.Json.Serialization; - - public class GlobalService - { - [JsonPropertyName("environments")] - public IEnumerable? Environments { get; set; } - } -} diff --git a/src/Infrastructure/Contracts/PBICloud/GlobalServiceEnvironment.cs b/src/Infrastructure/Contracts/PBICloud/GlobalServiceEnvironment.cs deleted file mode 100644 index 205b18b6..00000000 --- a/src/Infrastructure/Contracts/PBICloud/GlobalServiceEnvironment.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Sqlbi.Bravo.Infrastructure.Contracts.PBICloud -{ - using System.Collections.Generic; - using System.Diagnostics; - using System.Text.Json.Serialization; - - [DebuggerDisplay("{CloudName}")] - public class GlobalServiceEnvironment - { - [JsonPropertyName("cloudName")] - public string? CloudName { get; set; } - - [JsonPropertyName("services")] - public IEnumerable? Services { get; set; } - - [JsonPropertyName("clients")] - public IEnumerable? Clients { get; set; } - } -} diff --git a/src/Infrastructure/Contracts/PBICloud/GlobalServiceEnvironmentClient.cs b/src/Infrastructure/Contracts/PBICloud/GlobalServiceEnvironmentClient.cs deleted file mode 100644 index 9e1a44d4..00000000 --- a/src/Infrastructure/Contracts/PBICloud/GlobalServiceEnvironmentClient.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Sqlbi.Bravo.Infrastructure.Contracts.PBICloud -{ - using System.Text.Json.Serialization; - - public class GlobalServiceEnvironmentClient - { - [JsonPropertyName("name")] - public string? Name { get; set; } - - [JsonPropertyName("appId")] - public string? AppId { get; set; } - - [JsonPropertyName("redirectUri")] - public string? RedirectUri { get; set; } - } -} \ No newline at end of file diff --git a/src/Infrastructure/Contracts/PBICloud/GlobalServiceEnvironmentService.cs b/src/Infrastructure/Contracts/PBICloud/GlobalServiceEnvironmentService.cs deleted file mode 100644 index f30b46bd..00000000 --- a/src/Infrastructure/Contracts/PBICloud/GlobalServiceEnvironmentService.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Sqlbi.Bravo.Infrastructure.Contracts.PBICloud -{ - using System.Collections.Generic; - using System.Text.Json.Serialization; - - public class GlobalServiceEnvironmentService - { - [JsonPropertyName("name")] - public string? Name { get; set; } - - [JsonPropertyName("endpoint")] - public string? Endpoint { get; set; } - - [JsonPropertyName("resourceId")] - public string? ResourceId { get; set; } - - [JsonPropertyName("allowedDomains")] - public IEnumerable? AllowedDomains { get; set; } - } -} diff --git a/src/Infrastructure/Contracts/PBICloud/TenantCluster.cs b/src/Infrastructure/Contracts/PBICloud/TenantClusterContract.cs similarity index 79% rename from src/Infrastructure/Contracts/PBICloud/TenantCluster.cs rename to src/Infrastructure/Contracts/PBICloud/TenantClusterContract.cs index 3dd98d14..b14381f9 100644 --- a/src/Infrastructure/Contracts/PBICloud/TenantCluster.cs +++ b/src/Infrastructure/Contracts/PBICloud/TenantClusterContract.cs @@ -2,10 +2,10 @@ { using System.Text.Json.Serialization; - public class TenantCluster + internal sealed class TenantClusterContract { [JsonPropertyName("FixedClusterUri")] - public string? FixedClusterUri { get; set; } + public string FixedClusterUri { get; set; } = null!; //public string? PrivateLinkFixedClusterUri { get; set; } @@ -17,4 +17,4 @@ public class TenantCluster //public string? TenantId { get; set; } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/Infrastructure/Contracts/PBIConstants.cs b/src/Infrastructure/Contracts/PBIConstants.cs new file mode 100644 index 00000000..45b37808 --- /dev/null +++ b/src/Infrastructure/Contracts/PBIConstants.cs @@ -0,0 +1,17 @@ +namespace Sqlbi.Bravo.Infrastructure.Contracts +{ + internal static class PBIConstants + { + public static class Endpoints + { + public static readonly Uri GlobalCloudPowerBIUri = new("https://api.powerbi.com", UriKind.Absolute); + } + + public static class Registry + { + public const string PowerBIDiscoveryUrlValueName = "PowerBIDiscoveryUrl"; + public const string PowerBISubkeyName = @"SOFTWARE\Microsoft\Microsoft Power BI\"; + public const string PowerBIPolicySubkeyName = @"SOFTWARE\Policies\Microsoft\Microsoft Power BI\"; + } + } +} diff --git a/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs b/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..2b0ba5a9 --- /dev/null +++ b/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.DependencyInjection; +using Sqlbi.Bravo.Infrastructure.Services.PowerBI; + +namespace Sqlbi.Bravo.Infrastructure.Extensions +{ + internal static class ServiceCollectionExtensions + { + //public static void AddSingletonIfNotRegistered(this IServiceCollection services) + // where TService : class + // where TImplementation : class, TService + //{ + // if (!services.Any(sd => sd.ServiceType == typeof(TService))) + // { + // services.AddSingleton(); + // } + //} + + public static IServiceCollection AddPBICloudServices(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + return services; + } + } +} diff --git a/src/Infrastructure/Helpers/MsalHelper.cs b/src/Infrastructure/Helpers/MsalHelper.cs index 3037d5eb..6b6740d9 100644 --- a/src/Infrastructure/Helpers/MsalHelper.cs +++ b/src/Infrastructure/Helpers/MsalHelper.cs @@ -3,25 +3,20 @@ using Microsoft.Identity.Client; using Microsoft.Identity.Client.Desktop; using Sqlbi.Bravo.Infrastructure.Configuration; - using Sqlbi.Bravo.Infrastructure.Models; using Sqlbi.Bravo.Infrastructure.Models.PBICloud; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; internal static class MsalHelper { private const string SystemBrowserRedirectUri = "http://localhost"; private const string MicrosoftAccountOnlyQueryParameter = "msafed=0"; // Restrict logins to only AAD based organizational accounts - public static IPublicClientApplication CreatePublicClientApplication(IPBICloudEnvironment pbicloudEnvironment) + public static IPublicClientApplication CreatePublicClientApplication(CloudEnvironment environment) { var useEmbeddedBrowser = !UserPreferences.Current.UseSystemBrowserForAuthentication; - var redirectUri = (useEmbeddedBrowser ? pbicloudEnvironment.AzureADRedirectAddress : SystemBrowserRedirectUri); - var authorityUri = pbicloudEnvironment.AzureADAuthority; + var redirectUri = (useEmbeddedBrowser ? environment.RedirectUri : SystemBrowserRedirectUri); // TODO: should we add logging .WithLogging() ?? - var builder = PublicClientApplicationBuilder.Create(pbicloudEnvironment.AzureADClientId).WithAuthority(authorityUri).WithRedirectUri(redirectUri); + var builder = PublicClientApplicationBuilder.Create(environment.ClientId).WithAuthority(environment.AuthorityUri).WithRedirectUri(redirectUri); { if (useEmbeddedBrowser) builder.WithWindowsEmbeddedBrowserSupport(); @@ -33,11 +28,11 @@ public static IPublicClientApplication CreatePublicClientApplication(IPBICloudEn return publicClient; } - public static async Task AcquireTokenSilentAsync(string userPrincipalName, IPBICloudEnvironment environment, CancellationToken cancellationToken) + public static async Task AcquireTokenSilentAsync(string email, CloudEnvironment environment, CancellationToken cancellationToken) { var extraQueryParameters = MicrosoftAccountOnlyQueryParameter; - var scopes = environment.AzureADScopes; - var loginHint = userPrincipalName; + var scopes = new string[] { $"{environment.ResourceId}/.default" }; + var loginHint = email; var publicClient = CreatePublicClientApplication(environment); var msalAuthenticationResult = await publicClient.AcquireTokenSilent(scopes, loginHint).WithExtraQueryParameters(extraQueryParameters).ExecuteAsync(cancellationToken).ConfigureAwait(false); @@ -46,13 +41,13 @@ public static async Task AcquireTokenSilentAsync(string u return pbicloudAuthenticationResult; } - public static async Task AcquireTokenInteractiveAsync(string userPrincipalName, IPBICloudEnvironment environment, string claims, CancellationToken cancellationToken) + public static async Task AcquireTokenInteractiveAsync(string email, CloudEnvironment environment, string claims, CancellationToken cancellationToken) { var useEmbeddedBrowser = !UserPreferences.Current.UseSystemBrowserForAuthentication; var extraQueryParameters = MicrosoftAccountOnlyQueryParameter; var prompt = Prompt.SelectAccount; // Force a sign-in as the MSAL web browser might contain cookies for the current user and we don't necessarily want to re-sign-in the same user - var scopes = environment.AzureADScopes; - var loginHint = userPrincipalName; + var scopes = new string[] { $"{environment.ResourceId}/.default" }; + var loginHint = email; var acquireTokenTask = ProcessHelper.RunOnUISynchronizationContextContext(async () => { @@ -75,7 +70,7 @@ public static async Task AcquireTokenInteractiveAsync(str return pbicloudAuthenticationResult; } - //public static async Task AcquireTokenByIntegratedWindowsAuthAsync(IPBICloudEnvironment environment, CancellationToken cancellationToken) + //public static async Task AcquireTokenByIntegratedWindowsAuthAsync(IPBICloudEnvironment environment, CancellationToken cancellationToken) //{ // var publicClient = CreatePublicClientApplication(environment); // var msalAuthenticationResult = await publicClient.AcquireTokenByIntegratedWindowsAuth(environment.AzureADScopes).ExecuteAsync(cancellationToken).ConfigureAwait(false); @@ -87,7 +82,7 @@ public static async Task AcquireTokenInteractiveAsync(str // return pbicloudAuthenticationResult; //} - public static async Task ClearTokenCacheAsync(IPBICloudEnvironment environment) + public static async Task ClearTokenCacheAsync(CloudEnvironment environment) { var publicClient = CreatePublicClientApplication(environment); var cachedAccounts = (await publicClient.GetAccountsAsync().ConfigureAwait(false)).ToArray(); diff --git a/src/Infrastructure/Models/IAuthenticationResult.cs b/src/Infrastructure/Models/IAuthenticationResult.cs deleted file mode 100644 index 750e9ec6..00000000 --- a/src/Infrastructure/Models/IAuthenticationResult.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Sqlbi.Bravo.Infrastructure.Models -{ - using Sqlbi.Bravo.Models; - - public interface IAuthenticationResult - { - bool IsExpired { get; } - - string AccessToken { get; } - - IBravoAccount Account { get; } - } -} \ No newline at end of file diff --git a/src/Infrastructure/Models/PBICloud/CloudEnvironment.cs b/src/Infrastructure/Models/PBICloud/CloudEnvironment.cs new file mode 100644 index 00000000..132422b7 --- /dev/null +++ b/src/Infrastructure/Models/PBICloud/CloudEnvironment.cs @@ -0,0 +1,40 @@ +namespace Sqlbi.Bravo.Infrastructure.Models.PBICloud +{ + using Sqlbi.Bravo.Infrastructure.Contracts.PBICloud; + using Sqlbi.Bravo.Infrastructure.Extensions; + + [DebuggerDisplay("{Name}")] + public sealed record CloudEnvironment( + string Name, + string Description, + string AuthorityUri, + string ClientId, + string RedirectUri, + string ResourceId, + string BackendUri, + string ClusterUri) + { + public Uri GetBackendRequestUri(string path) + => new(new Uri(BackendUri), relativeUri: path); + + public string GetIdentityProvider() + => $"{AuthorityUri}, {ResourceId}, {ClientId}"; + + internal static CloudEnvironment FromContract(CloudEnvironmentContract contract) + { + var aadService = contract.Services.Single((s) => s.IsAad()); + var powerbiBackendService = contract.Services.Single((s) => s.IsPowerBIBackend()); + var powerbiDesktopClient = contract.Clients.Single((c) => c.IsPowerBIDesktop()); + + return new CloudEnvironment( + Name: contract.CloudName, + Description: contract.GetDescription(), + AuthorityUri: aadService.Endpoint, + ClientId: powerbiDesktopClient.AppId, + RedirectUri: powerbiDesktopClient.RedirectUri, + ResourceId: powerbiBackendService.ResourceId, + BackendUri: powerbiBackendService.Endpoint, + ClusterUri: string.Empty); + } + } +} diff --git a/src/Infrastructure/Models/PBICloud/PBICloudAuthenticationRequest.cs b/src/Infrastructure/Models/PBICloud/PBICloudAuthenticationRequest.cs deleted file mode 100644 index 94042943..00000000 --- a/src/Infrastructure/Models/PBICloud/PBICloudAuthenticationRequest.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Sqlbi.Bravo.Infrastructure.Models.PBICloud -{ - using System.ComponentModel.DataAnnotations; - using System.Text.Json.Serialization; - - public class PBICloudAuthenticationRequest - { - [Required] - [JsonPropertyName("userPrincipalName")] - public string? UserPrincipalName { get; set; } - - [Required] - [JsonPropertyName("environment")] - public PBICloudEnvironment? Environment { get; set; } - } -} diff --git a/src/Infrastructure/Models/PBICloud/PBICloudAuthenticationResult.cs b/src/Infrastructure/Models/PBICloud/PBICloudAuthenticationResult.cs index 4c5f7a70..8f3bec95 100644 --- a/src/Infrastructure/Models/PBICloud/PBICloudAuthenticationResult.cs +++ b/src/Infrastructure/Models/PBICloud/PBICloudAuthenticationResult.cs @@ -2,45 +2,23 @@ { using Microsoft.Identity.Client; using Sqlbi.Bravo.Models; - using System; - using System.Diagnostics; [DebuggerDisplay($"{{{ nameof(GetDebuggerDisplay) }(),nq}}")] - internal sealed class PBICloudAuthenticationResult : IAuthenticationResult + public sealed class PBICloudAuthenticationResult { private readonly AuthenticationResult _authenticationResult; public PBICloudAuthenticationResult(AuthenticationResult authenticationResult) { _authenticationResult = authenticationResult; - Account = new BravoAccount(authenticationResult); + Account = new AppAccount(authenticationResult); } public bool IsExpired => _authenticationResult.ExpiresOn < DateTimeOffset.UtcNow.AddMinutes(1); public string AccessToken => _authenticationResult.AccessToken; - public IBravoAccount Account { get; private set; } - - #region IEquatable - - public override bool Equals(object? obj) - { - return Equals(obj as PBICloudAuthenticationResult); - } - - public bool Equals(PBICloudAuthenticationResult? other) - { - return other != null && - Account.Identifier == other.Account.Identifier; - } - - public override int GetHashCode() - { - return HashCode.Combine(Account.Identifier); - } - - #endregion + public AppAccount Account { get; private set; } private string GetDebuggerDisplay() { diff --git a/src/Infrastructure/Models/PBICloud/PBICloudEnvironment.cs b/src/Infrastructure/Models/PBICloud/PBICloudEnvironment.cs deleted file mode 100644 index 937d0e67..00000000 --- a/src/Infrastructure/Models/PBICloud/PBICloudEnvironment.cs +++ /dev/null @@ -1,161 +0,0 @@ -namespace Sqlbi.Bravo.Infrastructure.Models.PBICloud -{ - using Sqlbi.Bravo.Infrastructure.Contracts.PBICloud; - using Sqlbi.Bravo.Infrastructure.Extensions; - using System; - using System.Diagnostics; - using System.Linq; - using System.Text.Json.Serialization; - - [DebuggerDisplay("{Type}, {AzureADAuthority}")] - public class PBICloudEnvironment: IPBICloudEnvironment - { - [JsonPropertyName("type")] - public PBICloudEnvironmentType Type { get; set; } - - [JsonPropertyName("name")] - public string? Name { get; set; } - - [JsonPropertyName("description")] - public string? Description { get; set; } - - [JsonPropertyName("aadAuthority")] - public string? AzureADAuthority { get; set; } - - [JsonPropertyName("aadClientId")] - public string? AzureADClientId { get; set; } - - [JsonPropertyName("aadRedirectAddress")] - public string? AzureADRedirectAddress { get; set; } - - [JsonPropertyName("aadResource")] - public string? AzureADResource { get; set; } - - [JsonPropertyName("aadScopes")] - public string[]? AzureADScopes { get; set; } - - [JsonPropertyName("serviceEndpoint")] - public string? ServiceEndpoint { get; set; } - - [JsonPropertyName("clusterEndpoint")] - public string? ClusterEndpoint { get; set; } - - [JsonPropertyName("identityProvider")] - public string? IdentityProvider => $"{AzureADAuthority}, {AzureADResource}, {AzureADClientId}"; - - [JsonIgnore] - public bool IsMicrosoftInternal => Type == PBICloudEnvironmentType.Custom && Name.EqualsI(PBICloudEnvironmentTypeExtensions.PpeCloudName); - - public Uri GetServiceEndpointUri(string path) - { - var baseUri = new Uri(ServiceEndpoint!, UriKind.Absolute); - return new Uri(baseUri, relativeUri: path); - } - - internal static PBICloudEnvironment CreateFrom(GlobalServiceEnvironment globalServiceEnvironment) - { - var azureActiveDirectoryService = globalServiceEnvironment.Services?.SingleOrDefault((s) => "aad".EqualsI(s.Name)); // AAD common - var powerbiBackendService = globalServiceEnvironment.Services?.SingleOrDefault((s) => "powerbi-backend".EqualsI(s.Name)); - var powerbiDesktopClient = globalServiceEnvironment.Clients?.SingleOrDefault((c) => "powerbi-desktop".EqualsI(c.Name)); - - var pbicloudEnvironment = new PBICloudEnvironment - { - Type = globalServiceEnvironment.CloudName.ToCloudEnvironmentType(), - Name = globalServiceEnvironment.CloudName, - Description = globalServiceEnvironment.CloudName.ToCloudEnvironmentDescription(), - AzureADAuthority = azureActiveDirectoryService?.Endpoint, - AzureADClientId = powerbiDesktopClient?.AppId, - AzureADRedirectAddress = powerbiDesktopClient?.RedirectUri, - AzureADResource = powerbiBackendService?.ResourceId, - AzureADScopes = null, - ServiceEndpoint = powerbiBackendService?.Endpoint, - ClusterEndpoint = null - }; - - if (pbicloudEnvironment.AzureADResource is not null) - pbicloudEnvironment.AzureADScopes = new string[] { $"{ pbicloudEnvironment.AzureADResource }/.default" }; - - return pbicloudEnvironment; - } - - public override bool Equals(object? obj) - { - return obj is PBICloudEnvironment environment && - Type == environment.Type && - Name == environment.Name && - AzureADAuthority == environment.AzureADAuthority && - AzureADClientId == environment.AzureADClientId && - AzureADRedirectAddress == environment.AzureADRedirectAddress && - AzureADResource == environment.AzureADResource && - ServiceEndpoint == environment.ServiceEndpoint; - } - - public override int GetHashCode() - { - return HashCode.Combine(Type, AzureADAuthority, AzureADClientId, AzureADRedirectAddress, AzureADResource, ServiceEndpoint); - } - } - - public interface IPBICloudEnvironment - { - /// - /// Type of the PowerBI environment. - /// - PBICloudEnvironmentType Type { get; set; } - - /// - /// Cloud environment name. - /// - string? Name { get; set; } - - /// - /// Cloud environment description. - /// - string? Description { get; set; } - - /// - /// Azure Active Directory Secure Token Service (STS) Authority - /// - string? AzureADAuthority { get; set; } - - /// - /// Azure Active Directory (AAD) ClientId for the AAD application - /// - string? AzureADClientId { get; set; } - - /// - /// Azure Active Directory (AAD) Redirect Address for AAD application - /// - string? AzureADRedirectAddress { get; set; } - - /// - /// Azure Active Directory Resource to authenticate against - /// - string? AzureADResource { get; set; } - - /// - /// Azure Active Directory scopes requested to access the protected - /// - string[]? AzureADScopes { get; set; } - - /// - /// Endpoint to communicate with the PowerBI service - /// - string? ServiceEndpoint { get; set; } - - /// - /// Fixed tenant cluster endpoint - /// - string? ClusterEndpoint { get; set; } - - /// - /// MSOLAP OLEDB provider 'Identity Provider' - /// - string? IdentityProvider { get; } - - [JsonIgnore] - bool IsMicrosoftInternal { get; } - - Uri GetServiceEndpointUri(string path); - } -} diff --git a/src/Infrastructure/Models/PBICloud/PBICloudEnvironmentType.cs b/src/Infrastructure/Models/PBICloud/PBICloudEnvironmentType.cs deleted file mode 100644 index 6972f321..00000000 --- a/src/Infrastructure/Models/PBICloud/PBICloudEnvironmentType.cs +++ /dev/null @@ -1,113 +0,0 @@ -namespace Sqlbi.Bravo.Infrastructure.Models.PBICloud -{ - using Sqlbi.Bravo.Infrastructure.Extensions; - using System; - - public enum PBICloudEnvironmentType - { - Unknown = 0, - - /// - /// Others - /// - Custom = 1, - - /// - /// GlobalService "cloudName": "GlobalCloud" - (Commercial Cloud), - /// - Public = 2, - - /// - /// GlobalService "cloudName": "GermanyCloud" - /// - Germany = 3, - - /// - /// GlobalService "cloudName": "ChinaCloud", - /// - China = 4, - - /// - /// GlobalService "cloudName": "USGovCloud" - (US Government Community Cloud) - /// - USGov = 5, - - /// - /// GlobalService "cloudName": "USGovDoDL4Cloud" - (US Government Community Cloud High) - /// - USGovHigh = 6, - - /// - /// GlobalService "cloudName": "USGovDoDL5Cloud" - (US Department of Defense) - /// - USGovMil = 7, - } - - internal static class PBICloudEnvironmentTypeExtensions - { - private const string GlobalCloudName = "GlobalCloud"; - private const string GermanyCloudName = "GermanyCloud"; - private const string ChinaCloudName = "ChinaCloud"; - private const string USGovCloudName = "USGovCloud"; - private const string USGovDoDL4CloudName = "USGovDoDL4Cloud"; - private const string USGovDoDL5CloudName = "USGovDoDL5Cloud"; - //private const string USNatCloudName = "USNatCloud"; - //private const string USSecCloudName = "USSecCloud"; - internal const string PpeCloudName = "PpeCloud"; - - // Uri strings from > globalservice/v202003/environments/discover?client=powerbi-msolap - internal const string GlobalCloudApiUri = "https://api.powerbi.com"; - private const string GermanyCloudApiUri = "https://api.powerbi.de"; - private const string ChinaCloudApiUri = "https://api.powerbi.cn"; - private const string USGovCloudApiUri = "https://api.powerbigov.us"; - private const string USGovDoDL4CloudApiUri = "https://api.high.powerbigov.us"; - private const string USGovDoDL5CloudApiUri = "https://api.mil.powerbigov.us"; - //private const string USNatCloudNameApiUri = "https://api.powerbi.eaglex.ic.gov"; - //private const string USSecCloudNameApiUri = "https://api.powerbi.microsoft.scloud"; - //private const string PpeCloudNameApiUri = "https://biazure-int-edog-redirect.analysis-df.windows.net"; - - public static Uri[] TrustedApiUris = new Uri[] - { - new Uri(GlobalCloudApiUri), - new Uri(GermanyCloudApiUri), - new Uri(ChinaCloudApiUri), - new Uri(USGovCloudApiUri), - new Uri(USGovDoDL4CloudApiUri), - new Uri(USGovDoDL5CloudApiUri), - }; - - public static PBICloudEnvironmentType ToCloudEnvironmentType(this string? cloudName) - { - var environmentType = cloudName switch - { - var name when name is null => PBICloudEnvironmentType.Unknown, - var name when GlobalCloudName.EqualsI(name) => PBICloudEnvironmentType.Public, - var name when GermanyCloudName.EqualsI(name) => PBICloudEnvironmentType.Germany, - var name when ChinaCloudName.EqualsI(name) => PBICloudEnvironmentType.China, - var name when USGovCloudName.EqualsI(name) => PBICloudEnvironmentType.USGov, - var name when USGovDoDL4CloudName.EqualsI(name) => PBICloudEnvironmentType.USGovHigh, - var name when USGovDoDL5CloudName.EqualsI(name) => PBICloudEnvironmentType.USGovMil, - _ => PBICloudEnvironmentType.Custom - }; - - return environmentType; - } - - public static string ToCloudEnvironmentDescription(this string? cloudName) - { - var environmentType = cloudName.ToCloudEnvironmentType(); - var environmentDescription = environmentType switch - { - PBICloudEnvironmentType.Public => "Power BI", - PBICloudEnvironmentType.Germany => "Power BI Germany", - PBICloudEnvironmentType.China => "Power BI China (operated by 21Vianet)", - PBICloudEnvironmentType.USGov => "Power BI for US Government", - PBICloudEnvironmentType.USGovHigh => "Power BI for US Government (L4)", - PBICloudEnvironmentType.USGovMil => "Power BI for US Government (L5)", - _ => $"{ environmentType } - { cloudName ?? "" }", - }; - - return environmentDescription; - } - } -} \ No newline at end of file diff --git a/src/Infrastructure/Serialization/PBIServiceJsonSerializer.cs b/src/Infrastructure/Serialization/PBIServiceJsonSerializer.cs new file mode 100644 index 00000000..127efc3f --- /dev/null +++ b/src/Infrastructure/Serialization/PBIServiceJsonSerializer.cs @@ -0,0 +1,16 @@ +namespace Sqlbi.Bravo.Infrastructure.Serialization +{ + internal static class PBIServiceJsonSerializer + { + private readonly static JsonSerializerOptions s_options + = new(JsonSerializerDefaults.Web); + + public static T Deserialize(string json) + { + return JsonSerializer.Deserialize(json, s_options) + ?? throw new InvalidOperationException($"The JSON content deserialized to a null '{typeof(T)}' instance."); + } + public static string Serialize(T value) + => JsonSerializer.Serialize(value, s_options); + } +} diff --git a/src/Infrastructure/Services/PowerBI/PBICloudAuthenticationService.cs b/src/Infrastructure/Services/PowerBI/PBICloudAuthenticationService.cs index 208255a0..584c3a05 100644 --- a/src/Infrastructure/Services/PowerBI/PBICloudAuthenticationService.cs +++ b/src/Infrastructure/Services/PowerBI/PBICloudAuthenticationService.cs @@ -3,45 +3,39 @@ using Microsoft.Identity.Client; using Sqlbi.Bravo.Infrastructure; using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Models; using Sqlbi.Bravo.Infrastructure.Models.PBICloud; using Sqlbi.Bravo.Models; - using System; - using System.Collections.Generic; - using System.Text.Json; - using System.Threading; - using System.Threading.Tasks; public interface IPBICloudAuthenticationService { - IAuthenticationResult? CurrentAuthentication { get; } + PBICloudAuthenticationResult? CurrentAuthentication { get; } - IPBICloudEnvironment? CurrentEnvironment { get; } + CloudEnvironment? CurrentEnvironment { get; } - Task> GetEnvironmentsAsync(string userPrincipalName, CancellationToken cancellationToken); + Task> GetEnvironmentsAsync(string email, CancellationToken cancellationToken); - Task SignInAsync(string userPrincipalName, IPBICloudEnvironment environment, CancellationToken cancellationToken); + Task SignInAsync(string email, CloudEnvironment environment, CancellationToken cancellationToken); Task SignOutAsync(CancellationToken cancellationToken); } internal class PBICloudAuthenticationService : IPBICloudAuthenticationService, IDisposable { - private readonly IPBICloudSettingsService _pbicloudSettings; + private readonly IPBICloudConfigurationService _pbicloudConfiguration; private readonly SemaphoreSlim _authenticationSemaphore = new(1, 1); - public PBICloudAuthenticationService(IPBICloudSettingsService pbicloudSetting) + public PBICloudAuthenticationService(IPBICloudConfigurationService pbicloudConfiguration) { - _pbicloudSettings = pbicloudSetting; + _pbicloudConfiguration = pbicloudConfiguration; } - public IAuthenticationResult? CurrentAuthentication { get; private set; } + public PBICloudAuthenticationResult? CurrentAuthentication { get; private set; } - public IPBICloudEnvironment? CurrentEnvironment { get; private set; } + public CloudEnvironment? CurrentEnvironment { get; private set; } - public async Task> GetEnvironmentsAsync(string userPrincipalName, CancellationToken cancellationToken) + public async Task> GetEnvironmentsAsync(string email, CancellationToken cancellationToken) { - var environments = await _pbicloudSettings.GetEnvironmentsAsync(userPrincipalName, cancellationToken).ConfigureAwait(false); + var environments = await _pbicloudConfiguration.DiscoverCloudEnvironmentsAsync(email, cancellationToken); if (AppEnvironment.IsDiagnosticLevelVerbose) AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{ nameof(PBICloudAuthenticationService) }.{ nameof(GetEnvironmentsAsync) }", JsonSerializer.Serialize(environments)); @@ -49,30 +43,22 @@ public async Task> GetEnvironmentsAsync(string return environments; } - public async Task SignInAsync(string userPrincipalName, IPBICloudEnvironment environment, CancellationToken cancellationToken) + public async Task SignInAsync(string email, CloudEnvironment environment, CancellationToken cancellationToken) { - await _authenticationSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + await _authenticationSemaphore.WaitAsync(cancellationToken); try { if (AppEnvironment.IsDiagnosticLevelVerbose) AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(PBICloudAuthenticationService)}.{nameof(SignInAsync)}", JsonSerializer.Serialize(environment)); using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - cancellationTokenSource.CancelAfter(AppEnvironment.MSALSignInTimeout); + cancellationTokenSource.CancelAfter(TimeSpan.FromMinutes(2)); - var previousAuthentication = CurrentAuthentication; - var previousEnvironment = CurrentEnvironment; + var authentication = await AcquireTokenAsync(email, environment, cancellationTokenSource.Token); + var clusterUri = await _pbicloudConfiguration.ResolveTenantClusterUriAsync(environment, authentication.AccessToken, cancellationToken); - CurrentAuthentication = await AcquireTokenAsync(userPrincipalName, environment, cancellationTokenSource.Token).ConfigureAwait(false); - CurrentEnvironment = environment; - - var environmentChanged = !CurrentEnvironment.Equals(previousEnvironment); - var authenticationChanged = !CurrentAuthentication.Equals(previousAuthentication); - if (authenticationChanged || environmentChanged || CurrentEnvironment.ClusterEndpoint is null) - { - var tenantCluster = await _pbicloudSettings.GetTenantClusterAsync(CurrentEnvironment, CurrentAuthentication.AccessToken, cancellationToken).ConfigureAwait(false); - CurrentEnvironment.ClusterEndpoint = tenantCluster.FixedClusterUri; - } + CurrentAuthentication = authentication; + CurrentEnvironment = environment with { ClusterUri = clusterUri }; } finally { @@ -82,12 +68,13 @@ public async Task SignInAsync(string userPrincipalName, IPBICloudEnvironment env public async Task SignOutAsync(CancellationToken cancellationToken) { - await _authenticationSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + await _authenticationSemaphore.WaitAsync(cancellationToken); try { - BravoUnexpectedException.ThrowIfNull(CurrentEnvironment); - - await MsalHelper.ClearTokenCacheAsync(CurrentEnvironment); + if (CurrentEnvironment is not null) + { + await MsalHelper.ClearTokenCacheAsync(CurrentEnvironment); + } CurrentAuthentication = null; CurrentEnvironment = null; @@ -98,38 +85,33 @@ public async Task SignOutAsync(CancellationToken cancellationToken) } } - private async Task AcquireTokenAsync(string userPrincipalName, IPBICloudEnvironment environment, CancellationToken cancellationToken) + private async Task AcquireTokenAsync(string email, CloudEnvironment environment, CancellationToken cancellationToken) { // TODO: Acquire a token using WAM https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-desktop-acquire-token-wam // TODO: Acquire a token using integrated Windows authentication https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-desktop-acquire-token-integrated-windows-authentication try { - var authenticationResult = await MsalHelper.AcquireTokenSilentAsync(userPrincipalName, environment, cancellationToken).ConfigureAwait(false); - return authenticationResult; + return await MsalHelper.AcquireTokenSilentAsync(email, environment, cancellationToken); //if (UserPreferences.Current.Experimental?.UseIntegratedWindowsAuthenticationSso == true) //{ - // var authenticationResult = await MsalHelper.AcquireTokenByIntegratedWindowsAuthAsync(environment, cancellationToken).ConfigureAwait(false); - // return authenticationResult; + // return await MsalHelper.AcquireTokenByIntegratedWindowsAuthAsync(environment, cancellationToken); //} } catch (MsalUiRequiredException msalUiRequiredException) { - var authenticationResult = await MsalHelper.AcquireTokenInteractiveAsync(userPrincipalName, environment, msalUiRequiredException.Claims, cancellationToken).ConfigureAwait(false); - return authenticationResult; + return await MsalHelper.AcquireTokenInteractiveAsync(email, environment, msalUiRequiredException.Claims, cancellationToken); } catch (MsalServiceException msalServiceException) { - var authenticationResult = await MsalHelper.AcquireTokenInteractiveAsync(userPrincipalName, environment, msalServiceException.Claims, cancellationToken).ConfigureAwait(false); - return authenticationResult; + return await MsalHelper.AcquireTokenInteractiveAsync(email, environment, msalServiceException.Claims, cancellationToken); } catch (MsalClientException) { //if (UserPreferences.Current.Experimental?.UseIntegratedWindowsAuthenticationSso == true) //{ - // var authenticationResult = await MsalHelper.AcquireTokenInteractiveAsync(environment, claims: null, cancellationToken).ConfigureAwait(false); - // return authenticationResult; + // return await MsalHelper.AcquireTokenInteractiveAsync(environment, claims: null, cancellationToken); //} throw; diff --git a/src/Infrastructure/Services/PowerBI/PBICloudConfigurationService.cs b/src/Infrastructure/Services/PowerBI/PBICloudConfigurationService.cs new file mode 100644 index 00000000..6525d7f2 --- /dev/null +++ b/src/Infrastructure/Services/PowerBI/PBICloudConfigurationService.cs @@ -0,0 +1,93 @@ +namespace Sqlbi.Bravo.Infrastructure.Services.PowerBI +{ + using Sqlbi.Bravo.Infrastructure; + using Sqlbi.Bravo.Infrastructure.Contracts; + using Sqlbi.Bravo.Infrastructure.Contracts.PBICloud; + using Sqlbi.Bravo.Infrastructure.Models.PBICloud; + using Sqlbi.Bravo.Infrastructure.Serialization; + using Sqlbi.Bravo.Models; + using System.Net; + using System.Net.Http; + using System.Net.Http.Headers; + using System.Web; + + internal interface IPBICloudConfigurationService + { + Task> DiscoverCloudEnvironmentsAsync(string email, CancellationToken cancellationToken); + Task ResolveTenantClusterUriAsync(CloudEnvironment environment, string accessToken, CancellationToken cancellationToken); + } + + internal sealed class PBICloudConfigurationService : IPBICloudConfigurationService + { + private readonly HttpClient _httpClient; + private readonly Uri _discoveryBaseUri; + + public PBICloudConfigurationService(IPBILocalConfigurationReader pbiLocalConfiguration) + { + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.Accept.Clear(); + _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + + _discoveryBaseUri = pbiLocalConfiguration.GetPowerBIServiceDiscoveryBaseUri() + ?? PBIConstants.Endpoints.GlobalCloudPowerBIUri; + } + + public async Task> DiscoverCloudEnvironmentsAsync(string email, CancellationToken cancellationToken) + { + var response = await DiscoverCloudEnvironmentsAsync(email, apiVersion: "v202408", cancellationToken); + if (response is null) + response = await DiscoverCloudEnvironmentsAsync(email, apiVersion: "v202003", cancellationToken); + + var environments = response?.Environments ?? []; + + return [.. environments + .Where((e) => !e.IsMicrosoftInternalCloud()) // Filter out Microsoft internal environments + .Select(CloudEnvironment.FromContract)]; + } + + public async Task ResolveTenantClusterUriAsync(CloudEnvironment environment, string accessToken, CancellationToken cancellationToken) + { + var requestUri = environment.GetBackendRequestUri("spglobalservice/GetOrInsertClusterUrisByTenantlocation"); + + using var httpRequest = new HttpRequestMessage(HttpMethod.Put, requestUri); + httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + httpRequest.Content = new StringContent(string.Empty, Encoding.UTF8, MediaTypeNames.Application.Json); + + using var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); + httpResponse.EnsureSuccessStatusCode(); + + var json = await httpResponse.Content.ReadAsStringAsync(cancellationToken); + + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(PBICloudConfigurationService)}.{nameof(ResolveTenantClusterUriAsync)}", json); + + var tenantCluster = PBIServiceJsonSerializer.Deserialize(json); + return tenantCluster.FixedClusterUri; + } + + private async Task DiscoverCloudEnvironmentsAsync(string email, string apiVersion, CancellationToken cancellationToken) + { + var relativeUri = FormattableString.Invariant($"powerbi/globalservice/{apiVersion}/environments/discover?user={HttpUtility.UrlEncode(email)}"); + var requestUri = new Uri(_discoveryBaseUri, relativeUri); + + using var httpRequest = new HttpRequestMessage(HttpMethod.Post, requestUri); + using var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); + + if (httpResponse.StatusCode == HttpStatusCode.OK) + { + var json = await httpResponse.Content.ReadAsStringAsync(cancellationToken); + + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(PBICloudConfigurationService)}.{nameof(DiscoverCloudEnvironmentsAsync)}()", content: json); + + return PBIServiceJsonSerializer.Deserialize(json); + } + else if (httpResponse.StatusCode == HttpStatusCode.NotFound) + { + return null; + } + + throw new HttpRequestException($"Unexpected response status code {(int)httpResponse.StatusCode} ({httpResponse.ReasonPhrase}) from environment discovery.", inner: null, httpResponse.StatusCode); + } + } +} diff --git a/src/Infrastructure/Services/PowerBI/PBICloudService.cs b/src/Infrastructure/Services/PowerBI/PBICloudService.cs index e5051eac..ca087d89 100644 --- a/src/Infrastructure/Services/PowerBI/PBICloudService.cs +++ b/src/Infrastructure/Services/PowerBI/PBICloudService.cs @@ -55,8 +55,8 @@ public PBICloudService(IAuthenticationService authenticationService, HttpClient _httpClient.DefaultRequestHeaders.Accept.Clear(); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _authenticationService.PBICloudAuthentication.AccessToken); - var requestPath = GetResourceUserPhotoRequestUri.FormatInvariant(_authenticationService.PBICloudAuthentication.Account.UserPrincipalName); - var requestUri = _authenticationService.PBICloudEnvironment.GetServiceEndpointUri(requestPath); + var requestPath = GetResourceUserPhotoRequestUri.FormatInvariant(_authenticationService.PBICloudAuthentication.Account.Email); + var requestUri = _authenticationService.PBICloudEnvironment.GetBackendRequestUri(requestPath); using var response = await _httpClient.GetAsync(requestUri).ConfigureAwait(false); if (response.IsSuccessStatusCode) @@ -105,7 +105,7 @@ private async Task> GetCloudWorkspacesAsync(Cancella _httpClient.DefaultRequestHeaders.Accept.Clear(); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _authenticationService.PBICloudAuthentication.AccessToken); - var baseUri = new Uri(_authenticationService.PBICloudEnvironment.ClusterEndpoint!); + var baseUri = new Uri(_authenticationService.PBICloudEnvironment.ClusterUri!); var requestUri = new Uri(baseUri, relativeUri: GetWorkspacesRequestUri); using var response = await _httpClient.GetAsync(requestUri, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); @@ -124,7 +124,7 @@ private async Task> GetCloudSharedModelsAsync(Canc _httpClient.DefaultRequestHeaders.Accept.Clear(); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _authenticationService.PBICloudAuthentication.AccessToken); - var baseUri = new Uri(_authenticationService.PBICloudEnvironment.ClusterEndpoint!); + var baseUri = new Uri(_authenticationService.PBICloudEnvironment.ClusterUri!); var requestUri = new Uri(baseUri, relativeUri: GetGallerySharedDatasetsRequestUri); using var response = await _httpClient.GetAsync(requestUri, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); diff --git a/src/Infrastructure/Services/PowerBI/PBICloudSettingsService.cs b/src/Infrastructure/Services/PowerBI/PBICloudSettingsService.cs deleted file mode 100644 index e3a07d3f..00000000 --- a/src/Infrastructure/Services/PowerBI/PBICloudSettingsService.cs +++ /dev/null @@ -1,124 +0,0 @@ -namespace Sqlbi.Bravo.Infrastructure.Services.PowerBI -{ - using Microsoft.Win32; - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.Contracts.PBICloud; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Models.PBICloud; - using Sqlbi.Bravo.Models; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net; - using System.Net.Http; - using System.Net.Http.Headers; - using System.Net.Mime; - using System.Text; - using System.Text.Json; - using System.Threading; - using System.Threading.Tasks; - using System.Web; - - public interface IPBICloudSettingsService - { - Task GetTenantClusterAsync(IPBICloudEnvironment environment, string accessToken, CancellationToken cancellationToken); - - Task> GetEnvironmentsAsync(string userPrincipalName, CancellationToken cancellationToken); - } - - internal class PBICloudSettingsService : IPBICloudSettingsService - { - private const string GlobalServiceEnvironmentsDiscoverUrl = "powerbi/globalservice/v202003/environments/discover?user={0}"; - private const string GlobalServiceGetOrInsertClusterUrisByTenantlocationUrl = "spglobalservice/GetOrInsertClusterUrisByTenantlocation"; - - private readonly Lazy _environmentDiscoverBaseUri; - private readonly HttpClient _httpClient; - - public PBICloudSettingsService() - { - _httpClient = new HttpClient(); - _httpClient.DefaultRequestHeaders.Accept.Clear(); - _environmentDiscoverBaseUri = new(() => GetEnvironmentDiscoveryBaseUri()); - } - - public async Task GetTenantClusterAsync(IPBICloudEnvironment environment, string accessToken, CancellationToken cancellationToken) - { - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - - var requestUri = environment.GetServiceEndpointUri(GlobalServiceGetOrInsertClusterUrisByTenantlocationUrl); - using var request = new HttpRequestMessage(HttpMethod.Put, requestUri); - request.Content = new StringContent(string.Empty, Encoding.UTF8, MediaTypeNames.Application.Json); - - using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{ nameof(PBICloudSettingsService) }.{ nameof(GetTenantClusterAsync) }", content); - - var tenantCluster = JsonSerializer.Deserialize(content, AppEnvironment.DefaultJsonOptions); - BravoUnexpectedException.ThrowIfNull(tenantCluster); - - return tenantCluster; - } - - public async Task> GetEnvironmentsAsync(string userPrincipalName, CancellationToken cancellationToken) - { - var relativeUri = GlobalServiceEnvironmentsDiscoverUrl.FormatInvariant(HttpUtility.UrlEncode(userPrincipalName)); - var requestUri = new Uri(_environmentDiscoverBaseUri.Value, relativeUri); - - using var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false); - - var environments = response.StatusCode switch - { - HttpStatusCode.NotFound => Array.Empty(), - HttpStatusCode.OK => await GetImpl(response).ConfigureAwait(false), - _ => throw new BravoUnexpectedInvalidOperationException($"Unsupported response status code {response.StatusCode} ({response.ReasonPhrase})") - }; - - return environments; - - static async Task> GetImpl(HttpResponseMessage response) - { - var content = await response.Content.ReadAsStringAsync(CancellationToken.None).ConfigureAwait(false); - - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(PBICloudSettingsService)}.{nameof(GetEnvironmentsAsync)}", content); - - var globalService = JsonSerializer.Deserialize(content, AppEnvironment.DefaultJsonOptions); - var environments = globalService?.Environments?.Select(PBICloudEnvironment.CreateFrom).Where((e) => !e.IsMicrosoftInternal).ToArray(); - - return environments ?? Array.Empty(); - } - } - - private static Uri GetEnvironmentDiscoveryBaseUri() - { - const string PowerBIDiscoveryUrl = "PowerBIDiscoveryUrl"; - - // https://docs.microsoft.com/en-us/power-bi/enterprise/service-govus-overview#sign-in-to-power-bi-for-us-government - // https://github.com/microsoft/Federal-Business-Applications/tree/main/whitepapers/power-bi-registry-settings - - var uriString = Registry.LocalMachine.GetStringValue(subkeyName: "SOFTWARE\\Microsoft\\Microsoft Power BI", valueName: PowerBIDiscoveryUrl); - - if (uriString is null) - uriString = Registry.LocalMachine.GetStringValue(subkeyName: "SOFTWARE\\WOW6432Node\\Policies\\Microsoft\\Microsoft Power BI", valueName: PowerBIDiscoveryUrl); - - if (uriString is null) - uriString = PBICloudEnvironmentTypeExtensions.GlobalCloudApiUri; - - if (Uri.TryCreate(uriString, UriKind.Absolute, out var discoveryUri) == false || PBICloudEnvironmentTypeExtensions.TrustedApiUris.Contains(discoveryUri) == false) - { - throw new BravoUnexpectedInvalidOperationException($"Unsupported Power BI environment discovery URL ({ uriString })"); - } - - var baseUri = new Uri(uriString, UriKind.Absolute); - - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Text, name: $"{ nameof(PBICloudSettingsService) }.{ nameof(GetEnvironmentDiscoveryBaseUri) }", content: baseUri.AbsoluteUri); - - return baseUri; - } - } -} diff --git a/src/Infrastructure/Services/PowerBI/PBILocalConfigurationReader.cs b/src/Infrastructure/Services/PowerBI/PBILocalConfigurationReader.cs new file mode 100644 index 00000000..a96c67fa --- /dev/null +++ b/src/Infrastructure/Services/PowerBI/PBILocalConfigurationReader.cs @@ -0,0 +1,61 @@ +namespace Sqlbi.Bravo.Infrastructure.Services.PowerBI +{ + using Microsoft.Win32; + using Sqlbi.Bravo.Infrastructure.Contracts; + using Sqlbi.Bravo.Infrastructure.Extensions; + + internal interface IPBILocalConfigurationReader + { + Uri? GetPowerBIServiceDiscoveryBaseUri(); + Uri? GetPowerBIServiceFixedClusterUri(); + } + + internal sealed class PBILocalConfigurationReader : IPBILocalConfigurationReader + { + /// + /// Gets the Power BI service discovery base URI from the local machine registry. + /// + /// See https://github.com/microsoft/Federal-Business-Applications/tree/main/whitepapers/power-bi-registry-settings + /// and https://docs.microsoft.com/en-us/power-bi/enterprise/service-govus-overview#sign-in-to-power-bi-for-us-government + /// + /// + public Uri? GetPowerBIServiceDiscoveryBaseUri() + { + var valueName = PBIConstants.Registry.PowerBIDiscoveryUrlValueName; + + // The value may have been written to either the 64-bit or the 32-bit (WOW6432Node) registry view, + // depending on the bitness of the Power BI Desktop build that set it. Check both views explicitly so + // the result does not depend on the bitness of this (Bravo) process. + var value = GetRegistryString(valueName, RegistryView.Registry64); + if (value is null) + value = GetRegistryString(valueName, RegistryView.Registry32); + + return value is null ? null : new Uri(value, UriKind.Absolute); + } + + public Uri? GetPowerBIServiceFixedClusterUri() + { + // `PowerBIServiceUrl` is not a documented registry key, but it is used by the + // PBI Desktop to override the default service URL for fixed cluster scenarios. + + return null; // Not implemented yet. + } + + private static string? GetRegistryString(string valueName, RegistryView view) + { + using var key = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view); + + // Try the policy override first + var value = key.GetStringValue(PBIConstants.Registry.PowerBIPolicySubkeyName, valueName); + if (!string.IsNullOrEmpty(value)) + return value; + + // Then try the standard subkey + value = key.GetStringValue(PBIConstants.Registry.PowerBISubkeyName, valueName); + if (!string.IsNullOrEmpty(value)) + return value; + + return null; + } + } +} diff --git a/src/Models/BravoAccount.cs b/src/Models/AppAccount.cs similarity index 53% rename from src/Models/BravoAccount.cs rename to src/Models/AppAccount.cs index 18cdd363..92268ab7 100644 --- a/src/Models/BravoAccount.cs +++ b/src/Models/AppAccount.cs @@ -1,41 +1,28 @@ -namespace Sqlbi.Bravo.Models +namespace Sqlbi.Bravo.Models { using Microsoft.Identity.Client; - using System.Text.Json.Serialization; - public interface IBravoAccount - { - string Identifier { get; set; } - - string UserPrincipalName { get; set; } - - string Username { get; set; } - } - - internal sealed class BravoAccount : IBravoAccount + public sealed class AppAccount { /// /// Unique identifier for the account /// - [JsonPropertyName("id")] public string Identifier { get; set; } /// /// User name in UserPrincipalName (UPN) format - e.g. john.doe@contoso.com /// - [JsonPropertyName("userPrincipalName")] - public string UserPrincipalName { get; set; } + public string Email { get; set; } /// /// Displayable user name (not guaranteed to be unique, it is mutable) /// - [JsonPropertyName("username")] public string Username { get; set; } - public BravoAccount(AuthenticationResult authenticationResult) + public AppAccount(AuthenticationResult authenticationResult) { Identifier = authenticationResult.Account.HomeAccountId.Identifier; - UserPrincipalName = authenticationResult.Account.Username; + Email = authenticationResult.Account.Username; Username = authenticationResult.ClaimsPrincipal.FindFirst((claim) => claim.Type == "name")?.Value ?? ""; } } diff --git a/src/Models/Authentication/AppAccountDto.cs b/src/Models/Authentication/AppAccountDto.cs new file mode 100644 index 00000000..91100d71 --- /dev/null +++ b/src/Models/Authentication/AppAccountDto.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Sqlbi.Bravo.Models.Authentication +{ + public sealed record AppAccountDto( + [Required] [property: JsonPropertyName("id")] string Identifier, + [Required] string Email, + [Required] string Username); + + internal static class AppAccountDtoMappingExtensions + { + internal static AppAccountDto ToDto(this AppAccount account) => new( + account.Identifier, + account.Email, + account.Username); + } +} diff --git a/src/Models/Authentication/CloudEnvironmentDto.cs b/src/Models/Authentication/CloudEnvironmentDto.cs new file mode 100644 index 00000000..c733dbfe --- /dev/null +++ b/src/Models/Authentication/CloudEnvironmentDto.cs @@ -0,0 +1,38 @@ +using Sqlbi.Bravo.Infrastructure.Models.PBICloud; +using System.ComponentModel.DataAnnotations; + +namespace Sqlbi.Bravo.Models.Authentication +{ + public sealed record CloudEnvironmentDto( + [Required] string Name, + [Required] string Description, + [Required] string AuthorityUri, + [Required] string ClientId, + [Required] string RedirectUri, + [Required] string ResourceId, + [Required] string BackendUri, + [Required(AllowEmptyStrings = true)] string ClusterUri); + + internal static class CloudEnvironmentDtoMappingExtensions + { + internal static CloudEnvironmentDto ToDto(this CloudEnvironment model) => new( + model.Name, + model.Description, + model.AuthorityUri, + model.ClientId, + model.RedirectUri, + model.ResourceId, + model.BackendUri, + model.ClusterUri); + + internal static CloudEnvironment ToModel(this CloudEnvironmentDto dto) => new( + dto.Name, + dto.Description, + dto.AuthorityUri, + dto.ClientId, + dto.RedirectUri, + dto.ResourceId, + dto.BackendUri, + dto.ClusterUri); + } +} diff --git a/src/Models/Authentication/GetEnvironmentsRequest.cs b/src/Models/Authentication/GetEnvironmentsRequest.cs new file mode 100644 index 00000000..a280ee98 --- /dev/null +++ b/src/Models/Authentication/GetEnvironmentsRequest.cs @@ -0,0 +1,7 @@ +using System.ComponentModel.DataAnnotations; + +namespace Sqlbi.Bravo.Models.Authentication +{ + public sealed record GetEnvironmentsRequest( + [Required] string Email); +} diff --git a/src/Models/Authentication/GetEnvironmentsResponse.cs b/src/Models/Authentication/GetEnvironmentsResponse.cs new file mode 100644 index 00000000..6fda7701 --- /dev/null +++ b/src/Models/Authentication/GetEnvironmentsResponse.cs @@ -0,0 +1,9 @@ +using Sqlbi.Bravo.Infrastructure.Models.PBICloud; + +namespace Sqlbi.Bravo.Models.Authentication +{ + public sealed class GetEnvironmentsResponse(IEnumerable environments) + { + public IReadOnlyList Environments { get; } = [.. environments.Select(e => e.ToDto())]; + } +} diff --git a/src/Models/Authentication/SignInRequest.cs b/src/Models/Authentication/SignInRequest.cs new file mode 100644 index 00000000..43367042 --- /dev/null +++ b/src/Models/Authentication/SignInRequest.cs @@ -0,0 +1,8 @@ +using System.ComponentModel.DataAnnotations; + +namespace Sqlbi.Bravo.Models.Authentication +{ + public sealed record SignInRequest( + [Required] string Email, + [Required] CloudEnvironmentDto Environment); +} diff --git a/src/Models/Authentication/SignInResponse.cs b/src/Models/Authentication/SignInResponse.cs new file mode 100644 index 00000000..7e5d55b9 --- /dev/null +++ b/src/Models/Authentication/SignInResponse.cs @@ -0,0 +1,7 @@ +namespace Sqlbi.Bravo.Models.Authentication +{ + public sealed class SignInResponse(AppAccount account) + { + public AppAccountDto Account { get; } = account.ToDto(); + } +} diff --git a/src/Models/PBICloudDataset.cs b/src/Models/PBICloudDataset.cs index 9835e2a4..dbe244b0 100644 --- a/src/Models/PBICloudDataset.cs +++ b/src/Models/PBICloudDataset.cs @@ -135,7 +135,7 @@ public override int GetHashCode() return hash.ToHashCode(); } - internal static PBICloudDataset CreateFrom(IPBICloudEnvironment environment, CloudWorkspace cloudWorkspace, CloudSharedModel cloudSharedModel) + internal static PBICloudDataset CreateFrom(CloudEnvironment environment, CloudWorkspace cloudWorkspace, CloudSharedModel cloudSharedModel) { BravoUnexpectedException.ThrowIfNull(cloudWorkspace); BravoUnexpectedException.ThrowIfNull(cloudSharedModel); @@ -149,11 +149,11 @@ internal static PBICloudDataset CreateFrom(IPBICloudEnvironment environment, Clo WorkspaceName = cloudWorkspace.Name.NullIfEmpty() ?? cloudSharedModel.WorkspaceName, WorkspaceObjectId = cloudWorkspace.ObjectId, Id = cloudModel.Id, - ServerName = CommonHelper.ChangeUriScheme(environment.ServiceEndpoint, PBICloudService.PBIDatasetProtocolScheme, ignorePort: true), + ServerName = CommonHelper.ChangeUriScheme(environment.BackendUri, PBICloudService.PBIDatasetProtocolScheme, ignorePort: true), DatabaseName = cloudModel.DBName, ExternalServerName = null, ExternalDatabaseName = null, - IdentityProvider = environment.IdentityProvider, + IdentityProvider = environment.GetIdentityProvider(), DisplayName = cloudModel.DisplayName, Description = cloudModel.Description, Owner = $"{ cloudModel.CreatorUser?.GivenName } { cloudModel.CreatorUser?.FamilyName }", @@ -171,7 +171,7 @@ internal static PBICloudDataset CreateFrom(IPBICloudEnvironment environment, Clo if (dataset.IsXmlaEndPointSupported) { - dataset.ExternalServerName = CommonHelper.ChangeUriScheme(environment.ClusterEndpoint, PBICloudService.PBIPremiumXmlaEndpointProtocolScheme, ignorePort: true); + dataset.ExternalServerName = CommonHelper.ChangeUriScheme(environment.ClusterUri, PBICloudService.PBIPremiumXmlaEndpointProtocolScheme, ignorePort: true); dataset.ExternalDatabaseName = cloudModel.DisplayName; } else if (dataset.IsOnPremModel == true) diff --git a/src/Scripts/controllers/auth.ts b/src/Scripts/controllers/auth.ts index 3c3829f9..69822519 100644 --- a/src/Scripts/controllers/auth.ts +++ b/src/Scripts/controllers/auth.ts @@ -8,26 +8,25 @@ import { Dispatchable } from '../helpers/dispatchable'; import { Utils } from '../helpers/utils'; import { host, telemetry } from '../main'; import { AppError } from '../model/exceptions'; -import { PBICloudEnvironment } from '../model/pbi-cloud'; +import { CloudEnvironment } from '../model/pbi-cloud'; import { CacheHelper } from './cache'; -import { PBICloudAutenthicationRequest } from './host'; export interface Account { id?: string - userPrincipalName?: string + email?: string username?: string avatar?: string } export interface ExtendedAccount extends Account { - environments?: PBICloudEnvironment[] + environments?: CloudEnvironment[] environmentName?: string } export interface SignInRequest { - userPrincipalName: string + email: string environmentName: string - environments: PBICloudEnvironment[] + environments: CloudEnvironment[] } export class Auth extends Dispatchable { @@ -47,7 +46,7 @@ export class Auth extends Dispatchable { let cachedAccount = this.getCachedAccount(); if (cachedAccount) this.signIn({ - userPrincipalName: cachedAccount.userPrincipalName, + email: cachedAccount.email, environments: cachedAccount.environments, environmentName: cachedAccount.environmentName }).catch(ignore => {}); @@ -70,7 +69,7 @@ export class Auth extends Dispatchable { let environment = request && request.environments && request.environments.find(env => env.name == request.environmentName); - return host.signIn(request ? { userPrincipalName: request.userPrincipalName, environment: environment } : null) + return host.signIn(request ? { email: request.email, environment: environment } : null) .then(account => { if (account) { this.account = account; diff --git a/src/Scripts/controllers/host.ts b/src/Scripts/controllers/host.ts index eb979490..8b4c0e10 100644 --- a/src/Scripts/controllers/host.ts +++ b/src/Scripts/controllers/host.ts @@ -21,7 +21,7 @@ import { strings } from '../model/strings'; import { LogMessageObj } from './logger'; import { DateConfiguration, DateTemplate, sanitizeTemplates, TableValidation } from '../model/dates'; import { ModelChanges } from '../model/model-changes'; -import { PBICloudEnvironment } from '../model/pbi-cloud'; +import { CloudEnvironment } from '../model/pbi-cloud'; import { PowerBISignin } from '../view/powerbi-signin'; import { DialogResponse } from '../view/dialog'; @@ -49,9 +49,15 @@ export interface ProblemDetails { traceId?: string } -export interface PBICloudAutenthicationRequest { - userPrincipalName: string - environment: PBICloudEnvironment +export interface HostSignInRequest { + email: string + environment: CloudEnvironment +} +export interface HostGetEnvironmentsResponse { + environments: CloudEnvironment[] +} +export interface HostSignInResponse { + account: Account } export interface FormatDaxRequest { options: FormatDaxRequestOptions @@ -289,10 +295,10 @@ export class Host extends Dispatchable { if (auth.account) { // Try automatic sign-in with saved account (if any) - const signinRequest: SignInRequest = { - userPrincipalName: auth.account.userPrincipalName, - environmentName: auth.account.environmentName, - environments: auth.account.environments + const signinRequest: SignInRequest = { + email: auth.account.email, + environmentName: auth.account.environmentName, + environments: auth.account.environments }; return auth.signIn(signinRequest) .then(()=>{ @@ -383,26 +389,28 @@ export class Host extends Dispatchable { /**** APIs ****/ /* Authentication */ - getEnvironments(userPrincipalName: string) { - return >this.apiCall("auth/powerbi/GetEnvironments", { userPrincipalName: userPrincipalName }, {}, false); + getEnvironments(email: string) { + return (>this.apiCall("auth/GetEnvironments", { email: email }, {}, false)) + .then(response => response.environments); } - signIn(request?: PBICloudAutenthicationRequest) { + signIn(request?: HostSignInRequest) { const logSettings: ApiLogSettings = {}; - return >this.apiCall("auth/powerbi/SignIn", request || {}, { method: "POST" }, false, logSettings); + return (>this.apiCall("auth/SignIn", request || {}, { method: "POST" }, false, logSettings)) + .then(response => response.account); } - /*signIn(userPrincipalName?: string) { + /*signIn(email?: string) { const logSettings: ApiLogSettings = {}; - return >this.apiCall("auth/powerbi/SignIn", userPrincipalName ? { userPrincipalName: userPrincipalName } : {}, {}, false, logSettings); + return >this.apiCall("auth/SignIn", email ? { email: email } : {}, {}, false, logSettings); }*/ signOut() { const logSettings: ApiLogSettings = {}; - return this.apiCall("auth/powerbi/SignOut", {}, {}, false, logSettings); + return this.apiCall("auth/SignOut", {}, {}, false, logSettings); } getUserAvatar() { diff --git a/src/Scripts/model/pbi-cloud.ts b/src/Scripts/model/pbi-cloud.ts index 652df87a..d1859b3a 100644 --- a/src/Scripts/model/pbi-cloud.ts +++ b/src/Scripts/model/pbi-cloud.ts @@ -4,26 +4,13 @@ * https://www.sqlbi.com */ -export interface PBICloudEnvironment { - type: PBICloudEnvironmentType - name?: string - description?: string - aadAuthority?: string - aadClientId?: string - aadRedirectAddress?: string - aadResource?: string - aadScopes?: string - serviceEndpoint?: string - clusterEndpoint?: string +export interface CloudEnvironment { + name: string + description: string + authorityUri: string + clientId: string + redirectUri: string + resourceId: string + backendUri: string + clusterUri: string } - -export enum PBICloudEnvironmentType { - Unknown = 0, - Custom = 1, - Public = 2, - Germany = 3, - China = 4, - USGov = 5, - USGovHigh = 6, - USGovMil = 7 -} \ No newline at end of file diff --git a/src/Scripts/model/pii.ts b/src/Scripts/model/pii.ts index affd3db6..bdcd323e 100644 --- a/src/Scripts/model/pii.ts +++ b/src/Scripts/model/pii.ts @@ -7,7 +7,7 @@ // Personal identifiable information - Used by anonymization const userPii = [ - "userPrincipalName", + "email", "username", "emailAddress" ]; diff --git a/src/Scripts/view/powerbi-signin.ts b/src/Scripts/view/powerbi-signin.ts index 17e315db..e2eac72f 100644 --- a/src/Scripts/view/powerbi-signin.ts +++ b/src/Scripts/view/powerbi-signin.ts @@ -10,7 +10,6 @@ import { strings } from '../model/strings'; import { Dialog, DialogResponse } from './dialog'; import { i18n } from '../model/i18n'; import { Loader } from '../helpers/loader'; -import { PBICloudEnvironment } from '../model/pbi-cloud'; import { SignInRequest } from '../controllers/auth'; import { AppError } from '../model/exceptions'; @@ -35,7 +34,7 @@ export class PowerBISignin extends Dialog { let html = `

${i18n(strings.powerBiSigninDescription)}

- +
`; this.body.insertAdjacentHTML("beforeend", html); @@ -48,7 +47,7 @@ export class PowerBISignin extends Dialog { if (cachedAccount) { this.data = { - userPrincipalName: cachedAccount.userPrincipalName, + email: cachedAccount.email, environments: cachedAccount.environments, environmentName: cachedAccount.environmentName }; @@ -96,7 +95,7 @@ export class PowerBISignin extends Dialog { if (environments && environments.length) { this.data = { - userPrincipalName: email, + email: email, environments: environments, environmentName: environments[0].name }; diff --git a/src/Services/AuthenticationService.cs b/src/Services/AuthenticationService.cs index cde8cd69..0443d490 100644 --- a/src/Services/AuthenticationService.cs +++ b/src/Services/AuthenticationService.cs @@ -1,25 +1,18 @@ namespace Sqlbi.Bravo.Services { using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.Models; using Sqlbi.Bravo.Infrastructure.Models.PBICloud; using Sqlbi.Bravo.Infrastructure.Services.PowerBI; - using System; - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; public interface IAuthenticationService { - IPBICloudEnvironment PBICloudEnvironment { get; } + CloudEnvironment PBICloudEnvironment { get; } - IAuthenticationResult PBICloudAuthentication { get; } + PBICloudAuthenticationResult PBICloudAuthentication { get; } Task IsPBICloudSignInRequiredAsync(CancellationToken cancellationToken); - Task> GetPBICloudEnvironmentsAsync(string userPrincipalName, CancellationToken cancellationToken); - - Task PBICloudSignInAsync(string userPrincipalName, IPBICloudEnvironment environment, CancellationToken cancellationToken); + Task PBICloudSignInAsync(string email, CloudEnvironment environment, CancellationToken cancellationToken); Task PBICloudSignOutAsync(CancellationToken cancellationToken); } @@ -33,7 +26,7 @@ public AuthenticationService(IPBICloudAuthenticationService pbicloudAuthenticati _pbicloudAuthenticationService = pbicloudAuthenticationService; } - public IPBICloudEnvironment PBICloudEnvironment + public CloudEnvironment PBICloudEnvironment { get { @@ -42,7 +35,7 @@ public IPBICloudEnvironment PBICloudEnvironment } } - public IAuthenticationResult PBICloudAuthentication + public PBICloudAuthenticationResult PBICloudAuthentication { get { @@ -63,23 +56,17 @@ public async Task IsPBICloudSignInRequiredAsync(CancellationToken cancella if (authentication.IsExpired) { - await PBICloudSignInAsync(authentication.Account.UserPrincipalName, environment, cancellationToken).ConfigureAwait(false); + await PBICloudSignInAsync(authentication.Account.Email, environment, cancellationToken).ConfigureAwait(false); } return false; } - public async Task> GetPBICloudEnvironmentsAsync(string userPrincipalName, CancellationToken cancellationToken) - { - var environments = await _pbicloudAuthenticationService.GetEnvironmentsAsync(userPrincipalName, cancellationToken).ConfigureAwait(false); - return environments; - } - - public async Task PBICloudSignInAsync(string userPrincipalName, IPBICloudEnvironment environment, CancellationToken cancellationToken) + public async Task PBICloudSignInAsync(string email, CloudEnvironment environment, CancellationToken cancellationToken) { try { - await _pbicloudAuthenticationService.SignInAsync(userPrincipalName, environment, cancellationToken).ConfigureAwait(false); + await _pbicloudAuthenticationService.SignInAsync(email, environment, cancellationToken); BravoUnexpectedException.Assert(_pbicloudAuthenticationService.CurrentAuthentication is not null); BravoUnexpectedException.Assert(_pbicloudAuthenticationService.CurrentEnvironment is not null); diff --git a/src/Startup.cs b/src/Startup.cs index 3f5e76f7..28cf8d6c 100644 --- a/src/Startup.cs +++ b/src/Startup.cs @@ -36,10 +36,7 @@ public void ConfigureServices(IServiceCollection services) services.AddOptions().Configure((settings) => settings.FromCommandLineArguments()); //.ValidateDataAnnotations(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -48,6 +45,7 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddPBICloudServices(); } public void Configure(IApplicationBuilder application, IWebHostEnvironment environment)