diff --git a/src/Svrooij.WinTuner.CmdLets/Commands/Deploy/DeployWtMsStoreApp.cs b/src/Svrooij.WinTuner.CmdLets/Commands/Deploy/DeployWtMsStoreApp.cs index 90c213a..4c7ed6a 100644 --- a/src/Svrooij.WinTuner.CmdLets/Commands/Deploy/DeployWtMsStoreApp.cs +++ b/src/Svrooij.WinTuner.CmdLets/Commands/Deploy/DeployWtMsStoreApp.cs @@ -144,7 +144,7 @@ protected override async Task ProcessAuthenticatedAsync(IAuthenticationProvider logger!.LogInformation("Uploading MSStore app {PackageId} to Intune", PackageId); proxyClient?.TriggerEvent(ConnectWtWinTuner.SessionId, nameof(DeployWtMsStoreApp), appVersion: ConnectWtWinTuner.AppVersion, packageId: PackageId, cancellationToken: CancellationToken.None); - var graphServiceClient = gcf!.CreateClient(provider); + var graphServiceClient = gcf!.CreateClient(provider, ConnectWtWinTuner.GraphEndpoint.GraphBetaEndpoint); try { var app = await graphStoreAppUploader!.CreateStoreAppAsync(graphServiceClient, PackageId!, cancellationToken); diff --git a/src/Svrooij.WinTuner.CmdLets/Commands/Deploy/DeployWtWin32App.cs b/src/Svrooij.WinTuner.CmdLets/Commands/Deploy/DeployWtWin32App.cs index d5dc881..375a6af 100644 --- a/src/Svrooij.WinTuner.CmdLets/Commands/Deploy/DeployWtWin32App.cs +++ b/src/Svrooij.WinTuner.CmdLets/Commands/Deploy/DeployWtWin32App.cs @@ -273,7 +273,7 @@ protected override async Task ProcessAuthenticatedAsync(IAuthenticationProvider } logger?.LogInformation("Uploading Win32App {DisplayName} to Intune with file {IntuneWinFile}", App!.DisplayName, IntuneWinFile); - var graphServiceClient = gcf!.CreateClient(provider); + var graphServiceClient = gcf!.CreateClient(provider, ConnectWtWinTuner.GraphEndpoint.GraphBetaEndpoint); if (IntuneWinFile is null) { diff --git a/src/Svrooij.WinTuner.CmdLets/Commands/Deploy/DeployWtWin32ContentVersion.cs b/src/Svrooij.WinTuner.CmdLets/Commands/Deploy/DeployWtWin32ContentVersion.cs index 60c694b..9dab253 100644 --- a/src/Svrooij.WinTuner.CmdLets/Commands/Deploy/DeployWtWin32ContentVersion.cs +++ b/src/Svrooij.WinTuner.CmdLets/Commands/Deploy/DeployWtWin32ContentVersion.cs @@ -69,7 +69,7 @@ protected override async Task ProcessAuthenticatedAsync(IAuthenticationProvider { logger?.LogInformation("Uploading file {IntuneWinFile} to {AppId}", IntuneWinFile, AppId); - var graphServiceClient = gcf!.CreateClient(provider); + var graphServiceClient = gcf!.CreateClient(provider, ConnectWtWinTuner.GraphEndpoint.GraphBetaEndpoint); if (IntuneWinFile is null) { diff --git a/src/Svrooij.WinTuner.CmdLets/Commands/Graph/ConnectWtWinTuner.cs b/src/Svrooij.WinTuner.CmdLets/Commands/Graph/ConnectWtWinTuner.cs index 90ffc48..7bfa5c2 100644 --- a/src/Svrooij.WinTuner.CmdLets/Commands/Graph/ConnectWtWinTuner.cs +++ b/src/Svrooij.WinTuner.CmdLets/Commands/Graph/ConnectWtWinTuner.cs @@ -10,6 +10,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; +using WingetIntune.Graph; namespace Svrooij.WinTuner.CmdLets.Commands.Graph; /// @@ -61,7 +62,6 @@ namespace Svrooij.WinTuner.CmdLets.Commands.Graph; public class ConnectWtWinTuner : DependencyCmdlet { private const string DefaultClientId = "d5a8a406-3b1d-4069-91cc-d76acdd812fe"; - private const string DefaultClientCredentialScope = "https://graph.microsoft.com/.default"; private const string ParamSetInteractive = "Interactive"; private const string ParamSetClientCredentials = "ClientCredentials"; private const string ParamSetClientCertificateCredentials = "ClientCertificateCredentials"; @@ -74,6 +74,7 @@ public class ConnectWtWinTuner : DependencyCmdlet internal static IAuthenticationProvider? AuthenticationProvider { get; private set; } internal static string? SessionId { get; private set; } internal static string AppVersion { get; } = typeof(ConnectWtWinTuner).Assembly.GetName().Version?.ToString() ?? "1.0.0"; + internal static GraphCloudEndpoint GraphEndpoint { get; private set; } = GraphCloudEnvironments.Global; /// /// Use a managed identity to connect to Intune @@ -85,7 +86,7 @@ public class ConnectWtWinTuner : DependencyCmdlet ValueFromPipeline = false, ValueFromPipelineByPropertyName = false, HelpMessage = "Use a managed identity to connect to Intune")] - public SwitchParameter UseManagedIdentity { get; set; } = Environment.GetEnvironmentVariable("AZURE_USE_MANAGED_IDENTITY")?.Equals("true", StringComparison.OrdinalIgnoreCase) == true; + public SwitchParameter UseManagedIdentity { get; set; } = System.Environment.GetEnvironmentVariable("AZURE_USE_MANAGED_IDENTITY")?.Equals("true", StringComparison.OrdinalIgnoreCase) == true; /// /// Use default Azure Credentials from Azure.Identity to connect to Intune @@ -97,7 +98,7 @@ public class ConnectWtWinTuner : DependencyCmdlet ValueFromPipeline = false, ValueFromPipelineByPropertyName = false, HelpMessage = "Use default Azure Credentials from Azure.Identity to connect to Intune")] - public SwitchParameter UseDefaultCredentials { get; set; } = Environment.GetEnvironmentVariable("AZURE_USE_DEFAULT_CREDENTIALS")?.Equals("true", StringComparison.OrdinalIgnoreCase) == true; + public SwitchParameter UseDefaultCredentials { get; set; } = System.Environment.GetEnvironmentVariable("AZURE_USE_DEFAULT_CREDENTIALS")?.Equals("true", StringComparison.OrdinalIgnoreCase) == true; /// /// Use a token from another source to connect to Intune @@ -109,7 +110,7 @@ public class ConnectWtWinTuner : DependencyCmdlet ValueFromPipeline = false, ValueFromPipelineByPropertyName = false, HelpMessage = "Use a token from another source to connect to Intune, this is the least preferred way to use")] - public string? Token { get; set; } = Environment.GetEnvironmentVariable("AZURE_TOKEN"); + public string? Token { get; set; } = System.Environment.GetEnvironmentVariable("AZURE_TOKEN"); /// /// @@ -159,7 +160,7 @@ public class ConnectWtWinTuner : DependencyCmdlet ValueFromPipeline = false, ValueFromPipelineByPropertyName = false, HelpMessage = "Specify the tenant ID. Loaded from `AZURE_TENANT_ID`")] - public string? TenantId { get; set; } = Environment.GetEnvironmentVariable("AZURE_TENANT_ID"); + public string? TenantId { get; set; } = System.Environment.GetEnvironmentVariable("AZURE_TENANT_ID"); /// /// @@ -185,7 +186,7 @@ public class ConnectWtWinTuner : DependencyCmdlet ValueFromPipeline = false, ValueFromPipelineByPropertyName = false, HelpMessage = "Specify the alternative client ID, optional. Loaded from `AZURE_CLIENT_ID`")] - public string? ClientId { get; set; } = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID"); + public string? ClientId { get; set; } = System.Environment.GetEnvironmentVariable("AZURE_CLIENT_ID"); /// @@ -198,7 +199,7 @@ public class ConnectWtWinTuner : DependencyCmdlet ValueFromPipeline = false, ValueFromPipelineByPropertyName = false, HelpMessage = "Specify the client secret. Loaded from `AZURE_CLIENT_SECRET`")] - public string? ClientSecret { get; set; } = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET"); + public string? ClientSecret { get; set; } = System.Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET"); /// /// Certificate Thumbprint for client authentication @@ -209,7 +210,7 @@ public class ConnectWtWinTuner : DependencyCmdlet ParameterSetName = ParamSetClientCertificateCredentials, ValueFromPipeline = false, HelpMessage = "Specify the thumbprint of the certificate. Loaded from `AZURE_CLIENT_CERT_THUMBPRINT`")] - public string? ClientCertificateThumbprint { get; set; } = Environment.GetEnvironmentVariable("AZURE_CLIENT_CERT_THUMBPRINT"); + public string? ClientCertificateThumbprint { get; set; } = System.Environment.GetEnvironmentVariable("AZURE_CLIENT_CERT_THUMBPRINT"); /// /// Specify scopes to use @@ -220,28 +221,28 @@ public class ConnectWtWinTuner : DependencyCmdlet ParameterSetName = ParamSetClientCredentials, ValueFromPipeline = false, ValueFromPipelineByPropertyName = false, - HelpMessage = "Specify the scopes to request, default is `https://graph.microsoft.com/.default`")] + HelpMessage = "Specify the scopes to request, default is the selected Microsoft Graph cloud `.default` scope")] [Parameter( Mandatory = false, Position = 10, ParameterSetName = nameof(UseDefaultCredentials), ValueFromPipeline = false, ValueFromPipelineByPropertyName = false, - HelpMessage = "Specify the scopes to request, default is `https://graph.microsoft.com/.default`")] + HelpMessage = "Specify the scopes to request, default is the selected Microsoft Graph cloud `.default` scope")] [Parameter( Mandatory = false, Position = 10, ParameterSetName = nameof(UseManagedIdentity), ValueFromPipeline = false, ValueFromPipelineByPropertyName = false, - HelpMessage = "Specify the scopes to request, default is `https://graph.microsoft.com/.default`")] + HelpMessage = "Specify the scopes to request, default is the selected Microsoft Graph cloud `.default` scope")] [Parameter( Mandatory = false, Position = 10, ParameterSetName = ParamSetClientCertificateCredentials, ValueFromPipeline = false, ValueFromPipelineByPropertyName = false, - HelpMessage = "Specify the scopes to request, default is `https://graph.microsoft.com/.default`")] + HelpMessage = "Specify the scopes to request, default is the selected Microsoft Graph cloud `.default` scope")] [Parameter( Mandatory = false, Position = 10, @@ -249,7 +250,14 @@ public class ConnectWtWinTuner : DependencyCmdlet ValueFromPipeline = false, ValueFromPipelineByPropertyName = false, HelpMessage = "Specify the scopes to request, default is `DeviceManagementConfiguration.ReadWrite.All`, `DeviceManagementApps.ReadWrite.All`")] - public string[]? Scopes { get; set; } = Environment.GetEnvironmentVariable("AZURE_SCOPES")?.Split(' '); + public string[]? Scopes { get; set; } = System.Environment.GetEnvironmentVariable("AZURE_SCOPES")?.Split(' '); + + /// + /// Select the Microsoft Graph cloud environment. + /// + [Parameter(Mandatory = false, Position = 12, HelpMessage = "Microsoft Graph cloud environment. Use USGov for GCC High.")] + [ValidateSet("Global", "USGov", "GCCHigh", "USGovDoD", "DoD", "China")] + public string Environment { get; set; } = System.Environment.GetEnvironmentVariable("WINTUNER_ENVIRONMENT") ?? "Global"; /// /// Immediately try to get a token. @@ -267,6 +275,8 @@ public class ConnectWtWinTuner : DependencyCmdlet public override async Task ProcessRecordAsync(CancellationToken cancellationToken) { SessionId ??= Guid.NewGuid().ToString(); + GraphEndpoint = GraphCloudEnvironments.GetEndpoint(Environment); + ValidateClientIdForCloud(); _logger?.LogInformation("Connecting to Intune using {ParameterSetName}", ParameterSetName); AuthenticationProvider = CreateAuthenticationProvider(cancellationToken); @@ -286,15 +296,23 @@ private IAuthenticationProvider CreateAuthenticationProvider(CancellationToken c return new WingetIntune.Internal.Msal.StaticAuthenticationProvider(Token); } - var scope = (Scopes ?? DefaultScopes)[0]; + var graphEndpoint = GraphEndpoint; + var clientCredentialScopes = Scopes ?? new[] { graphEndpoint.DefaultClientCredentialScope }; + var delegatedScopes = Scopes ?? DefaultScopes; if (UseManagedIdentity || UseDefaultCredentials) { // Maybe make which credentials to use configurable Azure.Core.TokenCredential credentials = UseManagedIdentity - ? new Azure.Identity.ManagedIdentityCredential(ClientId) - : new Azure.Identity.DefaultAzureCredential(); - return new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider(credentials, null, null, isCaeEnabled: false, DefaultClientCredentialScope); + ? new Azure.Identity.ManagedIdentityCredential(ClientId, new Azure.Identity.ManagedIdentityCredentialOptions + { + AuthorityHost = graphEndpoint.AuthorityHost, + }) + : new Azure.Identity.DefaultAzureCredential(new Azure.Identity.DefaultAzureCredentialOptions + { + AuthorityHost = graphEndpoint.AuthorityHost, + }); + return new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider(credentials, graphEndpoint.AllowedHosts, null, isCaeEnabled: false, scopes: clientCredentialScopes); } if (ParameterSetName == ParamSetClientCredentials) @@ -306,12 +324,13 @@ private IAuthenticationProvider CreateAuthenticationProvider(CancellationToken c new Azure.Identity.ClientSecretCredential(TenantId, ClientId, ClientSecret, new Azure.Identity.ClientSecretCredentialOptions { + AuthorityHost = graphEndpoint.AuthorityHost, TokenCachePersistenceOptions = new Azure.Identity.TokenCachePersistenceOptions { Name = "WinTuner-PowerShell-CC", UnsafeAllowUnencryptedStorage = true, } - }), isCaeEnabled: false, scopes: DefaultClientCredentialScope); + }), allowedHosts: graphEndpoint.AllowedHosts, isCaeEnabled: false, scopes: clientCredentialScopes); } else { @@ -345,13 +364,14 @@ private IAuthenticationProvider CreateAuthenticationProvider(CancellationToken c new Azure.Identity.ClientCertificateCredential(TenantId, ClientId, certificate, new Azure.Identity.ClientCertificateCredentialOptions { + AuthorityHost = graphEndpoint.AuthorityHost, TokenCachePersistenceOptions = new Azure.Identity.TokenCachePersistenceOptions { Name = "WinTuner-PowerShell-CC", UnsafeAllowUnencryptedStorage = true, } } - ), isCaeEnabled: false, scopes: DefaultClientCredentialScope); + ), allowedHosts: graphEndpoint.AllowedHosts, isCaeEnabled: false, scopes: clientCredentialScopes); } else @@ -371,6 +391,7 @@ private IAuthenticationProvider CreateAuthenticationProvider(CancellationToken c ClientId = ClientId ?? DefaultClientId, LoginHint = Username, RedirectUri = new Uri("http://localhost:12228/"), + AuthorityHost = graphEndpoint.AuthorityHost, TokenCachePersistenceOptions = new Azure.Identity.TokenCachePersistenceOptions { Name = "WinTuner-PowerShell", @@ -387,7 +408,7 @@ private IAuthenticationProvider CreateAuthenticationProvider(CancellationToken c // This will trigger the login screen early in the process. //var result = credential.Authenticate(new Azure.Core.TokenRequestContext(scopes!, tenantId: TenantId), cancellationToken); - return new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider(credential, isCaeEnabled: false, scopes: Scopes ?? DefaultScopes); + return new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider(credential, allowedHosts: graphEndpoint.AllowedHosts, isCaeEnabled: false, scopes: delegatedScopes); } return new WingetIntune.Internal.Msal.InteractiveAuthenticationProvider(new WingetIntune.Internal.Msal.InteractiveAuthenticationProviderOptions @@ -395,7 +416,8 @@ private IAuthenticationProvider CreateAuthenticationProvider(CancellationToken c ClientId = ClientId, TenantId = TenantId, Username = Username, - Scopes = Scopes ?? DefaultScopes, + Scopes = delegatedScopes, + Environment = graphEndpoint.Environment, }); } @@ -416,7 +438,7 @@ private IAuthenticationProvider CreateAuthenticationProvider(CancellationToken c throw new InvalidOperationException("AuthenticationProvider is not set, please run Connect-WtWinTuner first."); } // This is a "hack" to get a token from the authentication provider. - var ri = new RequestInformation(Method.GET, "https://graph.microsoft.com/test", new Dictionary()); + var ri = new RequestInformation(Method.GET, GraphEndpoint.TestEndpoint, new Dictionary()); await AuthenticationProvider.AuthenticateRequestAsync(ri, cancellationToken: cancellationToken); string? headerValue = ri.Headers.TryGetValue("Authorization", out var values) ? values.FirstOrDefault() : null; @@ -437,4 +459,14 @@ internal static Task ClearAuthentication(CancellationToken cancellationToken = d } private const string AuthenticationScheme = "Bearer"; + + private void ValidateClientIdForCloud() + { + if (GraphEndpoint.Environment != GraphCloudEnvironment.Global && + ParameterSetName == ParamSetInteractive && + string.IsNullOrWhiteSpace(ClientId)) + { + throw new ArgumentException($"A ClientId from an app registration in {GraphEndpoint.DisplayName} is required for interactive authentication.", nameof(ClientId)); + } + } } diff --git a/src/Svrooij.WinTuner.CmdLets/Commands/Graph/GetWtMobileApps.cs b/src/Svrooij.WinTuner.CmdLets/Commands/Graph/GetWtMobileApps.cs index 71f1fa8..5cc82f2 100644 --- a/src/Svrooij.WinTuner.CmdLets/Commands/Graph/GetWtMobileApps.cs +++ b/src/Svrooij.WinTuner.CmdLets/Commands/Graph/GetWtMobileApps.cs @@ -63,7 +63,7 @@ protected override async Task ProcessAuthenticatedAsync(Microsoft.Kiota.Abstract { logger.LogInformation("Getting MobileApps with name: {NameFiler}, isAssigned: {IsAssigned}", NameContains, IsAssigned); - var graphServiceClient = gcf.CreateClient(provider); + var graphServiceClient = gcf.CreateClient(provider, ConnectWtWinTuner.GraphEndpoint.GraphBetaEndpoint); proxyClient?.TriggerEvent( sessionId: ConnectWtWinTuner.SessionId, command: nameof(GetWtMobileApps), diff --git a/src/Svrooij.WinTuner.CmdLets/Commands/Graph/GetWtWin32Apps.cs b/src/Svrooij.WinTuner.CmdLets/Commands/Graph/GetWtWin32Apps.cs index 3d3e88e..44b20d2 100644 --- a/src/Svrooij.WinTuner.CmdLets/Commands/Graph/GetWtWin32Apps.cs +++ b/src/Svrooij.WinTuner.CmdLets/Commands/Graph/GetWtWin32Apps.cs @@ -89,7 +89,7 @@ protected override async Task ProcessAuthenticatedAsync(IAuthenticationProvider logger?.LogInformation("Getting list of published apps"); - var graphServiceClient = gcf!.CreateClient(provider); + var graphServiceClient = gcf!.CreateClient(provider, ConnectWtWinTuner.GraphEndpoint.GraphBetaEndpoint); proxyClient?.TriggerEvent( sessionId: ConnectWtWinTuner.SessionId, command: nameof(GetWtWin32Apps), diff --git a/src/Svrooij.WinTuner.CmdLets/Commands/Graph/RemoveWtWin32App.cs b/src/Svrooij.WinTuner.CmdLets/Commands/Graph/RemoveWtWin32App.cs index 46a066e..c298d69 100644 --- a/src/Svrooij.WinTuner.CmdLets/Commands/Graph/RemoveWtWin32App.cs +++ b/src/Svrooij.WinTuner.CmdLets/Commands/Graph/RemoveWtWin32App.cs @@ -43,7 +43,7 @@ protected override async Task ProcessAuthenticatedAsync(IAuthenticationProvider { logger?.LogInformation("Removing app {appId} from Intune", AppId); - var graphServiceClient = gcf!.CreateClient(provider); + var graphServiceClient = gcf!.CreateClient(provider, Graph.ConnectWtWinTuner.GraphEndpoint.GraphBetaEndpoint); // Load the app to get the relationships var app = await graphServiceClient.DeviceAppManagement.MobileApps[AppId].GetAsync(cancellationToken: cancellationToken); diff --git a/src/Svrooij.WinTuner.CmdLets/Commands/Graph/UpdateWtIntuneApp.cs b/src/Svrooij.WinTuner.CmdLets/Commands/Graph/UpdateWtIntuneApp.cs index 5810dca..4bb8948 100644 --- a/src/Svrooij.WinTuner.CmdLets/Commands/Graph/UpdateWtIntuneApp.cs +++ b/src/Svrooij.WinTuner.CmdLets/Commands/Graph/UpdateWtIntuneApp.cs @@ -77,7 +77,7 @@ protected override async Task ProcessAuthenticatedAsync(IAuthenticationProvider { logger?.LogInformation("Updating app {appId} in Intune", AppId); - var graphServiceClient = gcf!.CreateClient(provider); + var graphServiceClient = gcf!.CreateClient(provider, ConnectWtWinTuner.GraphEndpoint.GraphBetaEndpoint); if (Categories is not null && Categories.Any()) { diff --git a/src/WinTuner.Proxy.Client/WinTunerProxyClientExtensions.cs b/src/WinTuner.Proxy.Client/WinTunerProxyClientExtensions.cs index c6198f2..12b029d 100644 --- a/src/WinTuner.Proxy.Client/WinTunerProxyClientExtensions.cs +++ b/src/WinTuner.Proxy.Client/WinTunerProxyClientExtensions.cs @@ -18,7 +18,7 @@ public static class WinTunerProxyClientExtensions /// This is a fire-and-forget method, that triggers a task in the backend. public static void TriggerEvent(this WinTunerProxyClient? client, string? sessionId, string command, string? packageId = null, string? appVersion = null, CancellationToken cancellationToken = default) { - if (client is null) // || System.Environment.GetEnvironmentVariable(WINTUNER_TELEMETRY_OPT_OUT)?.Equals("1") == true) + if (client is null || IsTelemetryOptOutEnabled()) { return; } @@ -46,4 +46,11 @@ await client.Event.PostAsync(new Models.UsageEventRequest } }, cancellationToken); } + + private static bool IsTelemetryOptOutEnabled() + { + var value = System.Environment.GetEnvironmentVariable(WINTUNER_TELEMETRY_OPT_OUT); + return value?.Equals("1", StringComparison.OrdinalIgnoreCase) == true || + value?.Equals("true", StringComparison.OrdinalIgnoreCase) == true; + } } diff --git a/src/WingetIntune/Graph/GraphClientFactory.cs b/src/WingetIntune/Graph/GraphClientFactory.cs index a16e406..000b850 100644 --- a/src/WingetIntune/Graph/GraphClientFactory.cs +++ b/src/WingetIntune/Graph/GraphClientFactory.cs @@ -6,13 +6,14 @@ namespace WingetIntune.Graph; public class GraphClientFactory { private readonly System.Net.Http.HttpClient httpClient; + public GraphClientFactory(System.Net.Http.HttpClient httpClient) { this.httpClient = httpClient; } - public Microsoft.Graph.Beta.GraphServiceClient CreateClient(IAuthenticationProvider authenticationProvider) + public Microsoft.Graph.Beta.GraphServiceClient CreateClient(IAuthenticationProvider authenticationProvider, string? baseUrl = null) { - return new GraphServiceClient(httpClient: httpClient, authenticationProvider: authenticationProvider); + return new GraphServiceClient(httpClient: httpClient, authenticationProvider: authenticationProvider, baseUrl: baseUrl ?? GraphCloudEnvironments.Global.GraphBetaEndpoint); } } diff --git a/src/WingetIntune/Graph/GraphCloudEnvironment.cs b/src/WingetIntune/Graph/GraphCloudEnvironment.cs new file mode 100644 index 0000000..e64a3cb --- /dev/null +++ b/src/WingetIntune/Graph/GraphCloudEnvironment.cs @@ -0,0 +1,95 @@ +namespace WingetIntune.Graph; + +public enum GraphCloudEnvironment +{ + Global, + USGov, + USGovDoD, + China, +} + +public sealed record GraphCloudEndpoint( + GraphCloudEnvironment Environment, + string DisplayName, + Uri AuthorityHost, + string GraphHost, + string GraphRoot) +{ + public string GraphBetaEndpoint => $"{GraphRoot}/beta"; + + public string DefaultClientCredentialScope => $"{GraphRoot}/.default"; + + public string TestEndpoint => $"{GraphRoot}/test"; + + public string[] AllowedHosts => new[] { GraphHost }; + + public string GetAuthority(string? tenantId = null) + { + var tenant = string.IsNullOrWhiteSpace(tenantId) ? "organizations" : tenantId; + return $"{AuthorityHost.ToString().TrimEnd('/')}/{tenant}"; + } +} + +public static class GraphCloudEnvironments +{ + public static GraphCloudEndpoint Global { get; } = new( + GraphCloudEnvironment.Global, + "Global", + new Uri("https://login.microsoftonline.com"), + "graph.microsoft.com", + "https://graph.microsoft.com"); + + public static GraphCloudEndpoint USGov { get; } = new( + GraphCloudEnvironment.USGov, + "US Government L4 / GCC High", + new Uri("https://login.microsoftonline.us"), + "graph.microsoft.us", + "https://graph.microsoft.us"); + + public static GraphCloudEndpoint USGovDoD { get; } = new( + GraphCloudEnvironment.USGovDoD, + "US Government L5 / DoD", + new Uri("https://login.microsoftonline.us"), + "dod-graph.microsoft.us", + "https://dod-graph.microsoft.us"); + + public static GraphCloudEndpoint China { get; } = new( + GraphCloudEnvironment.China, + "China operated by 21Vianet", + new Uri("https://login.chinacloudapi.cn"), + "microsoftgraph.chinacloudapi.cn", + "https://microsoftgraph.chinacloudapi.cn"); + + public static GraphCloudEndpoint GetEndpoint(GraphCloudEnvironment environment) => environment switch + { + GraphCloudEnvironment.Global => Global, + GraphCloudEnvironment.USGov => USGov, + GraphCloudEnvironment.USGovDoD => USGovDoD, + GraphCloudEnvironment.China => China, + _ => throw new ArgumentOutOfRangeException(nameof(environment), environment, null), + }; + + public static GraphCloudEndpoint GetEndpoint(string? environment) => GetEndpoint(Parse(environment)); + + public static GraphCloudEnvironment Parse(string? environment) + { + if (string.IsNullOrWhiteSpace(environment)) + { + return GraphCloudEnvironment.Global; + } + + return Normalize(environment) switch + { + "GLOBAL" or "PUBLIC" or "AZUREPUBLIC" or "AZUREPUBLICCLOUD" => GraphCloudEnvironment.Global, + "USGOV" or "USGOVERNMENT" or "GCC HIGH" or "GCCHIGH" or "GCCH" => GraphCloudEnvironment.USGov, + "USGOVDOD" or "DOD" or "USGOVERNMENTDOD" => GraphCloudEnvironment.USGovDoD, + "CHINA" or "AZURECHINA" or "MOONCAKE" => GraphCloudEnvironment.China, + _ => throw new ArgumentException($"Unknown Graph cloud environment '{environment}'.", nameof(environment)), + }; + } + + private static string Normalize(string value) => + value.Trim().Replace("-", "", StringComparison.Ordinal) + .Replace("_", "", StringComparison.Ordinal) + .ToUpperInvariant(); +} diff --git a/src/WingetIntune/Graph/GraphServiceClientExtensions.cs b/src/WingetIntune/Graph/GraphServiceClientExtensions.cs index b105553..ec63d7b 100644 --- a/src/WingetIntune/Graph/GraphServiceClientExtensions.cs +++ b/src/WingetIntune/Graph/GraphServiceClientExtensions.cs @@ -22,7 +22,7 @@ public static class GraphServiceClientExtensions var requestInfo = new RequestInformation { HttpMethod = Method.POST, - URI = new Uri($"https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/{win32LobAppId}/microsoft.graph.win32LobApp/contentVersions"), + URI = new Uri($"{graphServiceClient.RequestAdapter.BaseUrl}/deviceAppManagement/mobileApps/{win32LobAppId}/microsoft.graph.win32LobApp/contentVersions"), }; requestInfo.Headers.Add("Content-Type", "application/json"); requestInfo.Content = new MemoryStream(Encoding.UTF8.GetBytes("{}")); @@ -41,7 +41,7 @@ public static class GraphServiceClientExtensions var requestInfo = new RequestInformation { HttpMethod = Method.POST, - URI = new Uri($"https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/{win32LobAppId}/microsoft.graph.win32LobApp/contentVersions/{contentVersionId}/files"), + URI = new Uri($"{graphServiceClient.RequestAdapter.BaseUrl}/deviceAppManagement/mobileApps/{win32LobAppId}/microsoft.graph.win32LobApp/contentVersions/{contentVersionId}/files"), }; requestInfo.SetContentFromParsable(graphServiceClient.RequestAdapter, "application/json", mobileAppContentFile); return graphServiceClient.RequestAdapter.SendAsync(requestInfo, MobileAppContentFile.CreateFromDiscriminatorValue, errorMapping: ErrorMapping, cancellationToken: cancellationToken); @@ -58,7 +58,7 @@ public static class GraphServiceClientExtensions var requestInfo = new RequestInformation { HttpMethod = Method.GET, - URI = new Uri($"https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/{win32LobAppId}/microsoft.graph.win32LobApp/contentVersions/{contentVersionId}/files/{mobileAppContentFileId}"), + URI = new Uri($"{graphServiceClient.RequestAdapter.BaseUrl}/deviceAppManagement/mobileApps/{win32LobAppId}/microsoft.graph.win32LobApp/contentVersions/{contentVersionId}/files/{mobileAppContentFileId}"), }; return graphServiceClient.RequestAdapter.SendAsync(requestInfo, MobileAppContentFile.CreateFromDiscriminatorValue, errorMapping: ErrorMapping, cancellationToken: cancellationToken); } @@ -115,7 +115,7 @@ public static Task Intune_CommitWin32LobAppContentVersionFileAsync(this GraphSer { HttpMethod = Method.POST, Content = new MemoryStream(data), - URI = new Uri($"https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/{win32LobAppId}/microsoft.graph.win32LobApp/contentVersions/{contentVersionId}/files/{mobileAppContentFileId}/commit"), + URI = new Uri($"{graphServiceClient.RequestAdapter.BaseUrl}/deviceAppManagement/mobileApps/{win32LobAppId}/microsoft.graph.win32LobApp/contentVersions/{contentVersionId}/files/{mobileAppContentFileId}/commit"), }; requestInfo.Headers.Add("Content-Type", "application/json"); return graphServiceClient.RequestAdapter.SendNoContentAsync(requestInfo, errorMapping: ErrorMapping, cancellationToken: cancellationToken); diff --git a/src/WingetIntune/Internal/Msal/InteractiveAuthenticationProvider.cs b/src/WingetIntune/Internal/Msal/InteractiveAuthenticationProvider.cs index 5d291fd..05322f2 100644 --- a/src/WingetIntune/Internal/Msal/InteractiveAuthenticationProvider.cs +++ b/src/WingetIntune/Internal/Msal/InteractiveAuthenticationProvider.cs @@ -6,6 +6,7 @@ using Microsoft.Kiota.Abstractions; using Microsoft.Kiota.Abstractions.Authentication; using System.Runtime.InteropServices; +using WingetIntune.Graph; namespace WingetIntune.Internal.Msal; @@ -13,6 +14,7 @@ public sealed class InteractiveAuthenticationProvider : IAuthenticationProvider, { private readonly InteractiveAuthenticationProviderOptions _options; private readonly IPublicClientApplication publicClientApplication; + private readonly GraphCloudEndpoint graphEndpoint; private bool CacheLoaded = false; private const string DefaultClientId = "d5a8a406-3b1d-4069-91cc-d76acdd812fe"; private AuthenticationResult? authenticationResult; @@ -28,6 +30,7 @@ public InteractiveAuthenticationProvider(InteractiveAuthenticationProviderOption _options = options; + graphEndpoint = GraphCloudEnvironments.GetEndpoint(_options.Environment); if (string.IsNullOrWhiteSpace(options.ClientId)) { _options.ClientId = DefaultClientId; @@ -35,17 +38,9 @@ public InteractiveAuthenticationProvider(InteractiveAuthenticationProviderOption var builder = PublicClientApplicationBuilder .Create(_options.ClientId) + .WithAuthority(graphEndpoint.GetAuthority(_options.TenantId)) .WithDefaultRedirectUri(); - if (!string.IsNullOrWhiteSpace(_options.TenantId)) - { - builder.WithTenantId(_options.TenantId); - } - else - { - builder.WithAuthority(AzureCloudInstance.AzurePublic, AadAuthorityAudience.AzureAdMultipleOrgs); - } - if (_options.UseBroker) { builder.WithBroker(new BrokerOptions(BrokerOptions.OperatingSystems.Windows) { Title = "WinTuner" }); @@ -132,7 +127,7 @@ public async Task AuthenticateRequestAsync(RequestInformation request, Dictionar if (request is null) throw new ArgumentNullException(nameof(request)); - if (request.URI.Host == "graph.microsoft.com") + if (request.URI.Host.Equals(graphEndpoint.GraphHost, StringComparison.OrdinalIgnoreCase)) { var token = await AccuireTokenAsync(_options.Scopes!, _options.TenantId, _options.Username, cancellationToken); var headers = new RequestHeaders @@ -156,4 +151,5 @@ public class InteractiveAuthenticationProviderOptions public string? Username { get; set; } public string? ClientId { get; set; } public string? TenantId { get; set; } + public GraphCloudEnvironment Environment { get; set; } = GraphCloudEnvironment.Global; } diff --git a/src/WingetIntune/Internal/Msal/PublicClientAuth.cs b/src/WingetIntune/Internal/Msal/PublicClientAuth.cs index f080108..ba14e2b 100644 --- a/src/WingetIntune/Internal/Msal/PublicClientAuth.cs +++ b/src/WingetIntune/Internal/Msal/PublicClientAuth.cs @@ -5,6 +5,7 @@ using Microsoft.Identity.Client.Extensions.Msal; using Microsoft.Kiota.Abstractions; using Microsoft.Kiota.Abstractions.Authentication; +using WingetIntune.Graph; namespace WingetIntune.Internal.Msal; @@ -12,6 +13,7 @@ public partial class PublicClientAuth : IAuthenticationProvider, IDisposable { private readonly PublicClientOptions _options; private readonly IPublicClientApplication publicClientApplication; + private readonly GraphCloudEndpoint graphEndpoint; private readonly ILogger logger; private bool CacheLoaded = false; private const string DefaultClientId = "d5a8a406-3b1d-4069-91cc-d76acdd812fe"; @@ -32,10 +34,13 @@ public PublicClientAuth(ILogger logger, IOptions logger, IOptions((sp, client) => { - client.BaseAddress = new Uri("https://graph.microsoft.com/beta"); + client.BaseAddress = new Uri(Graph.GraphCloudEnvironments.Global.GraphBetaEndpoint); }).AttachKiotaHandlers(); services.AddTransient(); diff --git a/tests/WingetIntune.Tests/GraphExtensions/GraphServiceExtensionsTests.cs b/tests/WingetIntune.Tests/GraphExtensions/GraphServiceExtensionsTests.cs index 7ce4f87..c9c52f6 100644 --- a/tests/WingetIntune.Tests/GraphExtensions/GraphServiceExtensionsTests.cs +++ b/tests/WingetIntune.Tests/GraphExtensions/GraphServiceExtensionsTests.cs @@ -37,7 +37,40 @@ public async Task Intune_CreateWin32LobAppContentVersionAsync_MakesCorrectReques } [Fact] - public async Task Intune_CreateWin32LobAppContentVersionFileAsync_MakesCorrectRequest() + public async Task Intune_CreateWin32LobAppContentVersionAsync_UsesConfiguredGraphBaseUrl() + { + var appId = Guid.NewGuid().ToString(); + var token = Guid.NewGuid().ToString(); + + var handler = Substitute.For(); + + var response = new HttpResponseMessage(HttpStatusCode.Created); + response.Content = new StringContent(@"{ + ""@odata.type"": ""#microsoft.graph.mobileAppContent"", + ""id"": ""1"" +}"); + response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + + handler.AddFakeResponse( + $"https://graph.microsoft.us/beta/deviceAppManagement/mobileApps/{appId}/microsoft.graph.win32LobApp/contentVersions", + HttpMethod.Post, + "{}", + response); + + var httpClient = new HttpClient(handler); + var graphServiceClient = new GraphServiceClient( + httpClient, + new WingetIntune.Internal.Msal.StaticAuthenticationProvider(token), + GraphCloudEnvironments.USGov.GraphBetaEndpoint); + + var result = await graphServiceClient.Intune_CreateWin32LobAppContentVersionAsync(appId, CancellationToken.None)!; + Assert.Equal("1", result!.Id); + } + + [Theory] + [InlineData("https://graph.microsoft.com/beta")] + [InlineData("https://graph.microsoft.us/beta")] + public async Task Intune_CreateWin32LobAppContentVersionFileAsync_MakesCorrectRequest(string graphBaseUrl) { var appId = Guid.NewGuid().ToString(); int contentVersionId = 1; @@ -63,13 +96,16 @@ public async Task Intune_CreateWin32LobAppContentVersionFileAsync_MakesCorrectRe response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); handler.AddFakeResponse( - $"https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/{appId}/microsoft.graph.win32LobApp/contentVersions/{contentVersionId}/files", + $"{graphBaseUrl}/deviceAppManagement/mobileApps/{appId}/microsoft.graph.win32LobApp/contentVersions/{contentVersionId}/files", HttpMethod.Post, "{\"name\":\"test\",\"size\":1}", response); var httpClient = new HttpClient(handler); - var graphServiceClient = new GraphServiceClient(httpClient, new WingetIntune.Internal.Msal.StaticAuthenticationProvider(token)); + var graphServiceClient = new GraphServiceClient( + httpClient, + new WingetIntune.Internal.Msal.StaticAuthenticationProvider(token), + graphBaseUrl); var mobileAppFileContent = new MobileAppContentFile { @@ -82,8 +118,10 @@ public async Task Intune_CreateWin32LobAppContentVersionFileAsync_MakesCorrectRe Assert.NotNull(result); } - [Fact] - public async Task Intune_CommitWin32LobAppContentVersionFileAsync_MakesCorrectRequest() + [Theory] + [InlineData("https://graph.microsoft.com/beta")] + [InlineData("https://graph.microsoft.us/beta")] + public async Task Intune_CommitWin32LobAppContentVersionFileAsync_MakesCorrectRequest(string graphBaseUrl) { var appId = Guid.NewGuid().ToString(); int contentVersionId = 1; @@ -100,13 +138,16 @@ public async Task Intune_CommitWin32LobAppContentVersionFileAsync_MakesCorrectRe response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); handler.AddFakeResponse( - $"https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/{appId}/microsoft.graph.win32LobApp/contentVersions/{contentVersionId}/files/{fileId}/commit", + $"{graphBaseUrl}/deviceAppManagement/mobileApps/{appId}/microsoft.graph.win32LobApp/contentVersions/{contentVersionId}/files/{fileId}/commit", HttpMethod.Post, @"{""fileEncryptionInfo"":{""encryptionKey"":""test"",""initializationVector"":null,""mac"":null,""macKey"":null,""profileIdentifier"":null,""fileDigest"":""test"",""fileDigestAlgorithm"":""test""}}", response); var httpClient = new HttpClient(handler); - var graphServiceClient = new GraphServiceClient(httpClient, new WingetIntune.Internal.Msal.StaticAuthenticationProvider(token)); + var graphServiceClient = new GraphServiceClient( + httpClient, + new WingetIntune.Internal.Msal.StaticAuthenticationProvider(token), + graphBaseUrl); var body = new WingetIntune.Graph.FileEncryptionInfo { @@ -118,8 +159,10 @@ public async Task Intune_CommitWin32LobAppContentVersionFileAsync_MakesCorrectRe await graphServiceClient.Intune_CommitWin32LobAppContentVersionFileAsync(appId, contentVersionId.ToString(), fileId, body, CancellationToken.None)!; } - [Fact] - public async Task Intune_GetWin32LobAppContentVersionFileAsync_MakesCorrectRequest() + [Theory] + [InlineData("https://graph.microsoft.com/beta")] + [InlineData("https://graph.microsoft.us/beta")] + public async Task Intune_GetWin32LobAppContentVersionFileAsync_MakesCorrectRequest(string graphBaseUrl) { var appId = Guid.NewGuid().ToString(); int contentVersionId = 1; @@ -146,12 +189,15 @@ public async Task Intune_GetWin32LobAppContentVersionFileAsync_MakesCorrectReque response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); handler.AddFakeResponse( - $"https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/{appId}/microsoft.graph.win32LobApp/contentVersions/{contentVersionId}/files/{fileId}", + $"{graphBaseUrl}/deviceAppManagement/mobileApps/{appId}/microsoft.graph.win32LobApp/contentVersions/{contentVersionId}/files/{fileId}", HttpMethod.Get, response); var httpClient = new HttpClient(handler); - var graphServiceClient = new GraphServiceClient(httpClient, new WingetIntune.Internal.Msal.StaticAuthenticationProvider(token)); + var graphServiceClient = new GraphServiceClient( + httpClient, + new WingetIntune.Internal.Msal.StaticAuthenticationProvider(token), + graphBaseUrl); var result = await graphServiceClient.Intune_GetWin32LobAppContentVersionFileAsync(appId, contentVersionId.ToString(), fileId, CancellationToken.None)!; Assert.NotNull(result);