diff --git a/src/GeneralTools/DataverseClient/Client/Auth/AuthProcessor.cs b/src/GeneralTools/DataverseClient/Client/Auth/AuthProcessor.cs index 8c44f61..15dcb51 100644 --- a/src/GeneralTools/DataverseClient/Client/Auth/AuthProcessor.cs +++ b/src/GeneralTools/DataverseClient/Client/Auth/AuthProcessor.cs @@ -70,6 +70,9 @@ internal async static Task ExecuteAuthenticateServ createdLogSource = true; logSink = new DataverseTraceLogger(); } + + // Set the logger in the MSAL Logger + MSALLoggerCallBack mSALLogger = new MSALLoggerCallBack(logSink); string Authority = string.Empty; string Resource = string.Empty; @@ -139,7 +142,7 @@ internal async static Task ExecuteAuthenticateServ .WithAuthority(Authority) .WithLegacyCacheCompatibility(false) .WithHttpClientFactory(new MSALHttpClientFactory()) - .WithLogging(MSALLoggerCallBack.Log); + .WithLogging(mSALLogger.Log); } // initialization of memory cache if its not already initialized. @@ -189,7 +192,7 @@ internal async static Task ExecuteAuthenticateServ }) .WithAuthority(Authority) .WithLegacyCacheCompatibility(false) - .WithLogging(MSALLoggerCallBack.Log); + .WithLogging(mSALLogger.Log); pApp = cApp.Build(); @@ -300,9 +303,7 @@ internal async static Task ObtainAccessTokenAsync( } else { -#pragma warning disable CS0618 // Type or member is obsolete - _authenticationResult = await publicAppClient.AcquireTokenByUsernamePassword(scopes, clientCredentials.UserName.UserName, ServiceClient.MakeSecureString(clientCredentials.UserName.Password)).ExecuteAsync().ConfigureAwait(false); -#pragma warning restore CS0618 // Type or member is obsolete + _authenticationResult = await publicAppClient.AcquireTokenByUsernamePassword(scopes, clientCredentials.UserName.UserName, clientCredentials.UserName.Password).ExecuteAsync().ConfigureAwait(false); } } else diff --git a/src/GeneralTools/DataverseClient/Client/Builder/AbstractClientRequestBuilder.cs b/src/GeneralTools/DataverseClient/Client/Builder/AbstractClientRequestBuilder.cs index 815bd44..082c6be 100644 --- a/src/GeneralTools/DataverseClient/Client/Builder/AbstractClientRequestBuilder.cs +++ b/src/GeneralTools/DataverseClient/Client/Builder/AbstractClientRequestBuilder.cs @@ -133,6 +133,16 @@ internal OrganizationRequest BuildRequest(OrganizationRequest request) parameters.Add(RequestBinderUtil.HEADERLIST, new Dictionary(_headers)); } + if (_crmUserId != null && _crmUserId != Guid.Empty) + { + parameters.Add(RequestHeaders.CALLER_OBJECT_ID_HTTP_HEADER, _crmUserId.Value); + } + + if (_aadOidId != null && _aadOidId != Guid.Empty) + { + parameters.Add(RequestHeaders.AAD_CALLER_OBJECT_ID_HTTP_HEADER, _aadOidId.Value); + } + request.Parameters.AddRange(parameters); // Clear in case this is reused. diff --git a/src/GeneralTools/DataverseClient/Client/ConnectionService.cs b/src/GeneralTools/DataverseClient/Client/ConnectionService.cs index bc3a745..38c2edd 100644 --- a/src/GeneralTools/DataverseClient/Client/ConnectionService.cs +++ b/src/GeneralTools/DataverseClient/Client/ConnectionService.cs @@ -365,7 +365,7 @@ internal bool CalledbyExecuteRequest /// /// Logging provider for DataverseConnectionServiceobject. /// - private DataverseTraceLogger logEntry { get; set; } + internal DataverseTraceLogger logEntry { get; set; } /// /// Returns Logs from this process. @@ -2472,18 +2472,14 @@ private bool ShouldRetryWebAPI(Exception ex, int retryCount, int maxRetryCount, errorCode == ((int)ErrorCodes.ThrottlingTimeExceededError).ToString() || errorCode == ((int)ErrorCodes.ThrottlingConcurrencyLimitExceededError).ToString()) { - if (errorCode == ((int)ErrorCodes.ThrottlingBurstRequestLimitExceededError).ToString()) - { - // Use Retry-After delay when specified - if (httpOperationException.Response.Headers.ContainsKey("Retry-After")) - _retryPauseTimeRunning = TimeSpan.Parse(httpOperationException.Response.Headers["Retry-After"].FirstOrDefault()); - else - _retryPauseTimeRunning = retryPauseTime.Add(TimeSpan.FromSeconds(Math.Pow(2, retryCount))); ; // default timespan with back off is response does not contain the tag.. + if (httpOperationException.Response.Headers.TryGetValue("Retry-After", out var retryAfter) && double.TryParse(retryAfter.FirstOrDefault(), out var retrySeconds)) + { + // Note: Retry-After header is in seconds. + _retryPauseTimeRunning = TimeSpan.FromSeconds(retrySeconds); } else - { - // else use exponential back off delay - _retryPauseTimeRunning = retryPauseTime.Add(TimeSpan.FromSeconds(Math.Pow(2, retryCount))); + { + _retryPauseTimeRunning = retryPauseTime.Add(TimeSpan.FromSeconds(Math.Pow(2, retryCount))); ; // default timespan with back off is response does not contain the tag.. } isThrottlingRetry = true; return true; diff --git a/src/GeneralTools/DataverseClient/Client/DataverseTelemetryBehaviors.cs b/src/GeneralTools/DataverseClient/Client/DataverseTelemetryBehaviors.cs index 6c9f510..7264d1a 100644 --- a/src/GeneralTools/DataverseClient/Client/DataverseTelemetryBehaviors.cs +++ b/src/GeneralTools/DataverseClient/Client/DataverseTelemetryBehaviors.cs @@ -46,7 +46,7 @@ public DataverseTelemetryBehaviors(ConnectionService cli) // reading overrides from app config if present.. // these values override the values that are set on the client from the server. - DataverseTraceLogger logg = new DataverseTraceLogger(); + DataverseTraceLogger logg = _callerCdsConnectionServiceHandler.logEntry; try { // Initialize user agent @@ -107,22 +107,18 @@ public DataverseTelemetryBehaviors(ConnectionService cli) if (_maxBufferPoolSize < MAXBUFFERPOOLDEFAULT) { _maxBufferPoolSize = -1; - logg.Log($"Failed to set MaxBufferPoolSizeOveride property. Value found: {maxBufferPoolSz}. Size must be larger then {MAXBUFFERPOOLDEFAULT}.", System.Diagnostics.TraceEventType.Warning); + logg.Log($"Failed to set MaxBufferPoolSizeOverride property. Value found: {maxBufferPoolSz}. Size must be larger then {MAXBUFFERPOOLDEFAULT}.", System.Diagnostics.TraceEventType.Warning); } } } else - logg.Log($"Failed to parse MaxBufferPoolSizeOveride property. Value found: {maxBufferPoolSz}. MaxReceivedMessageSizeOverride must be a valid integer.", System.Diagnostics.TraceEventType.Warning); + logg.Log($"Failed to parse MaxBufferPoolSizeOverride property. Value found: {maxBufferPoolSz}. MaxReceivedMessageSizeOverride must be a valid integer.", System.Diagnostics.TraceEventType.Warning); } } catch (Exception ex) { - logg.Log("Failed to process binding override properties, Only MaxFaultSizeOverride, MaxReceivedMessageSizeOverride and MaxBufferPoolSizeOveride are supported and must be integers.", System.Diagnostics.TraceEventType.Warning, ex); - } - finally - { - logg.Dispose(); + logg.Log("Failed to process binding override properties, Only MaxFaultSizeOverride, MaxReceivedMessageSizeOverride and MaxBufferPoolSizeOverride are supported and must be integers.", System.Diagnostics.TraceEventType.Warning, ex); } } @@ -258,6 +254,8 @@ public object BeforeSendRequest(ref Message request, IClientChannel channel) httpRequestMessage = new HttpRequestMessageProperty(); } + string[] CrmUserIdList = null; + string[] AADOidList = null; if (httpRequestMessage != null) { httpRequestMessage.Headers[Utilities.RequestHeaders.USER_AGENT_HTTP_HEADER] = _userAgent; @@ -282,27 +280,68 @@ public object BeforeSendRequest(ref Message request, IClientChannel channel) Utilities.CleanUpHeaderKeys(httpRequestMessage.Headers); if (httpRequestMessageObject == null) request.Properties.Add(HttpRequestMessageProperty.Name, httpRequestMessage); + + CrmUserIdList = httpRequestMessage.Headers.GetValues(Utilities.RequestHeaders.CALLER_OBJECT_ID_HTTP_HEADER); + AADOidList = httpRequestMessage.Headers.GetValues(Utilities.RequestHeaders.AAD_CALLER_OBJECT_ID_HTTP_HEADER); + } // Adding SOAP headers Guid callerId = Guid.Empty; - if (_callerCdsConnectionServiceHandler != null) + Guid AADOId = Guid.Empty; + if (CrmUserIdList != null && CrmUserIdList.Length > 0) + { + if(!Guid.TryParse(CrmUserIdList[0], out callerId)) + _callerCdsConnectionServiceHandler.logEntry.Log("Failed to parse Caller Object ID from the HTTP Header.", System.Diagnostics.TraceEventType.Warning); + CrmUserIdList = null; // Clear the list. + } + + if (AADOidList != null && AADOidList.Length > 0) { - if (_callerCdsConnectionServiceHandler.WebClient != null) - callerId = _callerCdsConnectionServiceHandler.WebClient.CallerId; - if (_callerCdsConnectionServiceHandler.OnPremClient != null) - callerId = _callerCdsConnectionServiceHandler.OnPremClient.CallerId; + if (!Guid.TryParse(AADOidList[0], out AADOId)) + _callerCdsConnectionServiceHandler.logEntry.Log("Failed to parse AADObjectID from the HTTP Header.", System.Diagnostics.TraceEventType.Warning); + AADOidList = null; // Clear the list. } - if (callerId == Guid.Empty) // Prefer the Caller ID over the AADObjectID. + if (callerId == Guid.Empty && AADOId == Guid.Empty) + { + if (_callerCdsConnectionServiceHandler != null) + { + if (_callerCdsConnectionServiceHandler.WebClient != null) + callerId = _callerCdsConnectionServiceHandler.WebClient.CallerId; + if (_callerCdsConnectionServiceHandler.OnPremClient != null) + callerId = _callerCdsConnectionServiceHandler.OnPremClient.CallerId; + } + + if (callerId == Guid.Empty) // Prefer the Caller ID over the AADObjectID. + { + if (_callerCdsConnectionServiceHandler != null && (_callerCdsConnectionServiceHandler.CallerAADObjectId.HasValue && _callerCdsConnectionServiceHandler.CallerAADObjectId.Value != Guid.Empty)) + { + // Add Caller ID to the SOAP Envelope. + // Set a header request with the AAD Caller Object ID. + using (OperationContextScope scope = new OperationContextScope((IContextChannel)channel)) + { + var AADCallerIdHeader = new MessageHeader(_callerCdsConnectionServiceHandler.CallerAADObjectId.Value).GetUntypedHeader(Utilities.RequestHeaders.AAD_CALLER_OBJECT_ID_HTTP_HEADER, "http://schemas.microsoft.com/xrm/2011/Contracts"); + request.Headers.Add(AADCallerIdHeader); + } + } + } + } + else { - if (_callerCdsConnectionServiceHandler != null && (_callerCdsConnectionServiceHandler.CallerAADObjectId.HasValue && _callerCdsConnectionServiceHandler.CallerAADObjectId.Value != Guid.Empty)) + if ( callerId != Guid.Empty ) + { + using (OperationContextScope scope = new OperationContextScope((IContextChannel)channel)) + { + var CallerIdHeader = new MessageHeader(callerId).GetUntypedHeader(Xrm.Sdk.Client.SdkHeaders.CallerId, Xrm.Sdk.XmlNamespaces.V5.Contracts); + request.Headers.Add(CallerIdHeader); + } + } + else if (AADOId != Guid.Empty) { - // Add Caller ID to the SOAP Envelope. - // Set a header request with the AAD Caller Object ID. using (OperationContextScope scope = new OperationContextScope((IContextChannel)channel)) { - var AADCallerIdHeader = new MessageHeader(_callerCdsConnectionServiceHandler.CallerAADObjectId.Value).GetUntypedHeader(Utilities.RequestHeaders.AAD_CALLER_OBJECT_ID_HTTP_HEADER, "http://schemas.microsoft.com/xrm/2011/Contracts"); + var AADCallerIdHeader = new MessageHeader(AADOId).GetUntypedHeader(Utilities.RequestHeaders.AAD_CALLER_OBJECT_ID_HTTP_HEADER, "http://schemas.microsoft.com/xrm/2011/Contracts"); request.Headers.Add(AADCallerIdHeader); } } diff --git a/src/GeneralTools/DataverseClient/Client/ServiceClient.cs b/src/GeneralTools/DataverseClient/Client/ServiceClient.cs index 02c0a40..d241467 100644 --- a/src/GeneralTools/DataverseClient/Client/ServiceClient.cs +++ b/src/GeneralTools/DataverseClient/Client/ServiceClient.cs @@ -2039,11 +2039,10 @@ private bool ShouldRetry(OrganizationRequest req, Exception ex, int retryCount, OrgEx.Detail.ErrorCode == ErrorCodes.ThrottlingTimeExceededError || OrgEx.Detail.ErrorCode == ErrorCodes.ThrottlingConcurrencyLimitExceededError) { - // Error was raised by a instance throttle trigger. - if (OrgEx.Detail.ErrorCode == ErrorCodes.ThrottlingBurstRequestLimitExceededError) - { - // Use Retry-After delay when specified - _retryPauseTimeRunning = (TimeSpan)OrgEx.Detail.ErrorDetails["Retry-After"]; + // Use Retry-After delay when specified + if (OrgEx.Detail.ErrorDetails.TryGetValue("Retry-After", out var retryAfter) && retryAfter is TimeSpan retryAsTimeSpan) + { + _retryPauseTimeRunning = retryAsTimeSpan; } else { diff --git a/src/GeneralTools/DataverseClient/Client/Utils/MSALLoggerCallBack.cs b/src/GeneralTools/DataverseClient/Client/Utils/MSALLoggerCallBack.cs index a4e779f..50bfd29 100644 --- a/src/GeneralTools/DataverseClient/Client/Utils/MSALLoggerCallBack.cs +++ b/src/GeneralTools/DataverseClient/Client/Utils/MSALLoggerCallBack.cs @@ -8,15 +8,22 @@ namespace Microsoft.PowerPlatform.Dataverse.Client.Utils /// /// This class will be used to support hooking into MSAL Call back logic. /// - internal static class MSALLoggerCallBack + internal class MSALLoggerCallBack { - private static DataverseTraceLogger _logEntry; + + public DataverseTraceLogger LogSink { get; set; } = null; /// /// Enabled PII logging for this connection. /// if this flag is set, it will override the value from app config. /// - public static bool? EnabledPIILogging { get; set; } = null; + public bool? EnabledPIILogging { get; set; } = null; + + public MSALLoggerCallBack(DataverseTraceLogger logSink = null, bool? enabledPIILogging = null) + { + LogSink = logSink; + EnabledPIILogging = enabledPIILogging; + } /// /// @@ -24,15 +31,19 @@ internal static class MSALLoggerCallBack /// /// /// - static public void Log(LogLevel level, string message, bool containsPii) + public void Log(LogLevel level, string message, bool containsPii) { - if (_logEntry == null) - _logEntry = new DataverseTraceLogger(typeof(LogCallback).Assembly.GetName().Name); // set up logging client. + bool createdLogSource = false; + if (LogSink == null) + { + createdLogSource = true; + LogSink = new DataverseTraceLogger(typeof(LogCallback).Assembly.GetName().Name); // set up logging client. + } if (!EnabledPIILogging.HasValue) { EnabledPIILogging = ClientServiceProviders.Instance.GetService>().Value.MSALEnabledLogPII; - _logEntry.Log($"Setting MSAL PII Logging Feature to {EnabledPIILogging.Value}", System.Diagnostics.TraceEventType.Information); + LogSink.Log($"Setting MSAL PII Logging Feature to {EnabledPIILogging.Value}", System.Diagnostics.TraceEventType.Information); } if (containsPii && !EnabledPIILogging.Value) @@ -41,25 +52,30 @@ static public void Log(LogLevel level, string message, bool containsPii) } // Add (PII) prefix to messages that have PII in them per AAD Message alert. - message = containsPii ? $"(PII){message}" : message; + message = containsPii ? $"(PII){message}" : message; switch (level) { case LogLevel.Info: - _logEntry.Log(message, System.Diagnostics.TraceEventType.Information); + LogSink.Log(message, System.Diagnostics.TraceEventType.Information); break; case LogLevel.Verbose: - _logEntry.Log(message, System.Diagnostics.TraceEventType.Verbose); + LogSink.Log(message, System.Diagnostics.TraceEventType.Verbose); break; case LogLevel.Warning: - _logEntry.Log(message, System.Diagnostics.TraceEventType.Warning); + LogSink.Log(message, System.Diagnostics.TraceEventType.Warning); break; case LogLevel.Error: - _logEntry.Log(message, System.Diagnostics.TraceEventType.Error); + LogSink.Log(message, System.Diagnostics.TraceEventType.Error); break; default: break; } + + if (createdLogSource) + { + LogSink.Dispose(); + } } } diff --git a/src/GeneralTools/DataverseClient/Client/Utils/RequestBinderUtil.cs b/src/GeneralTools/DataverseClient/Client/Utils/RequestBinderUtil.cs index 1f04730..9c771ca 100644 --- a/src/GeneralTools/DataverseClient/Client/Utils/RequestBinderUtil.cs +++ b/src/GeneralTools/DataverseClient/Client/Utils/RequestBinderUtil.cs @@ -44,11 +44,23 @@ internal static void ProcessRequestBinderProperties(HttpRequestMessageProperty h } continue; } + if (itm.Key == Utilities.RequestHeaders.CALLER_OBJECT_ID_HTTP_HEADER) + { + AddorUpdateHeaderProperties(httpRequestMessageHeaders, Utilities.RequestHeaders.CALLER_OBJECT_ID_HTTP_HEADER, itm.Value.ToString()); + continue; + } + if (itm.Key == Utilities.RequestHeaders.AAD_CALLER_OBJECT_ID_HTTP_HEADER) + { + AddorUpdateHeaderProperties(httpRequestMessageHeaders, Utilities.RequestHeaders.AAD_CALLER_OBJECT_ID_HTTP_HEADER, itm.Value.ToString()); + continue; + } } if ( request.Parameters.Count > 0 ) { request.Parameters.Remove(Utilities.RequestHeaders.X_MS_CORRELATION_REQUEST_ID); request.Parameters.Remove(Utilities.RequestHeaders.X_MS_CLIENT_SESSION_ID); + request.Parameters.Remove(Utilities.RequestHeaders.CALLER_OBJECT_ID_HTTP_HEADER); + request.Parameters.Remove(Utilities.RequestHeaders.AAD_CALLER_OBJECT_ID_HTTP_HEADER); request.Parameters.Remove(HEADERLIST); } } diff --git a/src/GeneralTools/DataverseClient/ConnectControl/ServerLoginControl.xaml b/src/GeneralTools/DataverseClient/ConnectControl/ServerLoginControl.xaml index 396bb77..04dcf25 100644 --- a/src/GeneralTools/DataverseClient/ConnectControl/ServerLoginControl.xaml +++ b/src/GeneralTools/DataverseClient/ConnectControl/ServerLoginControl.xaml @@ -211,9 +211,9 @@ - + diff --git a/src/GeneralTools/DataverseClient/ConnectControl/ServerLoginControl.xaml.cs b/src/GeneralTools/DataverseClient/ConnectControl/ServerLoginControl.xaml.cs index 6db7658..aae2af1 100644 --- a/src/GeneralTools/DataverseClient/ConnectControl/ServerLoginControl.xaml.cs +++ b/src/GeneralTools/DataverseClient/ConnectControl/ServerLoginControl.xaml.cs @@ -23,6 +23,7 @@ using System.Media; using System.Windows.Automation.Peers; using System.Security.Policy; +using Microsoft.PowerPlatform.Dataverse.Client.Utils; #endregion @@ -260,8 +261,21 @@ public void SetGlobalStoreAccess(ConnectionManager connectionManager) ConnectionManager = connectionManager; - // Set the CRM Server List here from the UI.. - object oCrmDiscoServices = FindResource("OnlineDiscoveryServersDataSource"); + + bool hideOnPrem = AppSettingsHelper.GetAppSetting("HideOnPrem", true); + if (hideOnPrem) + { + rbOnPrem.IsEnabled = false; + rbOnPrem.Visibility = Visibility.Collapsed; + } + else + { + rbOnPrem.IsEnabled = true; + rbOnPrem.Visibility = Visibility.Visible; + } + + // Set the CRM Server List here from the UI.. + object oCrmDiscoServices = FindResource("OnlineDiscoveryServersDataSource"); if (oCrmDiscoServices != null && oCrmDiscoServices is Model.OnlineDiscoveryServers) ConnectionManager.OnlineDiscoveryServerList = (Model.OnlineDiscoveryServers)oCrmDiscoServices; diff --git a/src/GeneralTools/DataverseClient/DataverseClientWithConnector.sln b/src/GeneralTools/DataverseClient/DataverseClientWithConnector.sln index 795a215..0c60704 100644 --- a/src/GeneralTools/DataverseClient/DataverseClientWithConnector.sln +++ b/src/GeneralTools/DataverseClient/DataverseClientWithConnector.sln @@ -23,8 +23,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerPlatform.Dat EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerPlatform.Dataverse.WebResourceUtility", "WebResourceUtility\Microsoft.PowerPlatform.Dataverse.WebResourceUtility.csproj", "{FDFD6B7F-A925-40EE-98DC-2E06C1D1E3B6}" EndProject -#Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerPlatform.Dataverse.ServiceClientConverter", "Extensions\Microsoft.PowerPlatform.Dataverse.ServiceClientConverter\Microsoft.PowerPlatform.Dataverse.ServiceClientConverter.csproj", "{6F7522A6-3B98-40EE-B92B-5EA423E6F600}" -#EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.PowerPlatform.Dataverse.Client.AzAuth", "Extensions\Microsoft.PowerPlatform.Dataverse.Client.AzAuth\Microsoft.PowerPlatform.Dataverse.Client.AzAuth.csproj", "{618D52B7-4CE7-402F-972B-E381C1C28299}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution CRMINTERNAL|Any CPU = CRMINTERNAL|Any CPU @@ -131,18 +131,18 @@ Global {FDFD6B7F-A925-40EE-98DC-2E06C1D1E3B6}.Release|Any CPU.Build.0 = Release|Any CPU {FDFD6B7F-A925-40EE-98DC-2E06C1D1E3B6}.Release|x64.ActiveCfg = Release|x64 {FDFD6B7F-A925-40EE-98DC-2E06C1D1E3B6}.Release|x64.Build.0 = Release|x64 - {6F7522A6-3B98-40EE-B92B-5EA423E6F600}.CRMINTERNAL|Any CPU.ActiveCfg = Debug|Any CPU - {6F7522A6-3B98-40EE-B92B-5EA423E6F600}.CRMINTERNAL|Any CPU.Build.0 = Debug|Any CPU - {6F7522A6-3B98-40EE-B92B-5EA423E6F600}.CRMINTERNAL|x64.ActiveCfg = Debug|Any CPU - {6F7522A6-3B98-40EE-B92B-5EA423E6F600}.CRMINTERNAL|x64.Build.0 = Debug|Any CPU - {6F7522A6-3B98-40EE-B92B-5EA423E6F600}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6F7522A6-3B98-40EE-B92B-5EA423E6F600}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6F7522A6-3B98-40EE-B92B-5EA423E6F600}.Debug|x64.ActiveCfg = Debug|Any CPU - {6F7522A6-3B98-40EE-B92B-5EA423E6F600}.Debug|x64.Build.0 = Debug|Any CPU - {6F7522A6-3B98-40EE-B92B-5EA423E6F600}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6F7522A6-3B98-40EE-B92B-5EA423E6F600}.Release|Any CPU.Build.0 = Release|Any CPU - {6F7522A6-3B98-40EE-B92B-5EA423E6F600}.Release|x64.ActiveCfg = Release|Any CPU - {6F7522A6-3B98-40EE-B92B-5EA423E6F600}.Release|x64.Build.0 = Release|Any CPU + {618D52B7-4CE7-402F-972B-E381C1C28299}.CRMINTERNAL|Any CPU.ActiveCfg = Debug|Any CPU + {618D52B7-4CE7-402F-972B-E381C1C28299}.CRMINTERNAL|Any CPU.Build.0 = Debug|Any CPU + {618D52B7-4CE7-402F-972B-E381C1C28299}.CRMINTERNAL|x64.ActiveCfg = Debug|Any CPU + {618D52B7-4CE7-402F-972B-E381C1C28299}.CRMINTERNAL|x64.Build.0 = Debug|Any CPU + {618D52B7-4CE7-402F-972B-E381C1C28299}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {618D52B7-4CE7-402F-972B-E381C1C28299}.Debug|Any CPU.Build.0 = Debug|Any CPU + {618D52B7-4CE7-402F-972B-E381C1C28299}.Debug|x64.ActiveCfg = Debug|Any CPU + {618D52B7-4CE7-402F-972B-E381C1C28299}.Debug|x64.Build.0 = Debug|Any CPU + {618D52B7-4CE7-402F-972B-E381C1C28299}.Release|Any CPU.ActiveCfg = Release|Any CPU + {618D52B7-4CE7-402F-972B-E381C1C28299}.Release|Any CPU.Build.0 = Release|Any CPU + {618D52B7-4CE7-402F-972B-E381C1C28299}.Release|x64.ActiveCfg = Release|Any CPU + {618D52B7-4CE7-402F-972B-E381C1C28299}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.Client.AzAuth/AzAuth.cs b/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.Client.AzAuth/AzAuth.cs new file mode 100644 index 0000000..ff72d4b --- /dev/null +++ b/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.Client.AzAuth/AzAuth.cs @@ -0,0 +1,195 @@ +using Azure.Core; +using Azure.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.PowerPlatform.Dataverse.Client.Model; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace Microsoft.PowerPlatform.Dataverse.Client +{ + /// + /// Base auth class to create an authentication client for Az Authentication + /// This module will provide a means to create an Dataverse Service Client using Az Authentication + /// + public class AzAuth + { + + private DefaultAzureCredential _defaultAzureCredential; + private DefaultAzureCredentialOptions _credentialOptions; + private readonly bool _autoResolveAuthorityAndTenant; + private Dictionary> _scopesList; + private Dictionary _cacheList; + private ILogger _logger; + + + /// + /// Creates a new instance of the ServiceClient class + /// + /// + /// + /// + /// + /// + /// + public static ServiceClient CreateServiceClient(string instanceUrl, bool autoResolveAuthorityAndTenant = true, ILogger logger = null, DefaultAzureCredentialOptions credentialOptions = null) + { + if (!Uri.IsWellFormedUriString(instanceUrl, UriKind.RelativeOrAbsolute)) + { + throw new ArgumentException("Invalid instance URL"); + } + AzAuth azAuth = new AzAuth(autoResolveAuthorityAndTenant, credentialOptions , logger); + return new ServiceClient(new Uri(instanceUrl), tokenProviderFunction: azAuth.GetAccessToken, logger: logger); + } + + /// + /// Build this based on connection and configuration options. + /// + /// + /// + /// + /// + /// + /// + public static ServiceClient CreateServiceClient(ConnectionOptions connectionOptions, ConfigurationOptions configurationOptions = null, bool autoResolveAuthorityAndTenant = true, DefaultAzureCredentialOptions credentialOptions = null) + { + if ( connectionOptions == null ) + { + throw new ArgumentException("ConnectionOptions are required"); + } + if (connectionOptions.ServiceUri == null) + { + throw new ArgumentException("ConnectionOptions.ServiceUri is required"); + } + connectionOptions.AuthenticationType = AuthenticationType.ExternalTokenManagement; // force the authentication type to be external token management. + + AzAuth azAuth = new AzAuth(autoResolveAuthorityAndTenant, credentialOptions, connectionOptions.Logger); + connectionOptions.AccessTokenProviderFunctionAsync = azAuth.GetAccessToken; + return new ServiceClient(connectionOptions, false, configurationOptions); + } + + + + /// + /// Creates a new instance of the AzAuth class + /// + /// + /// + /// + public AzAuth(bool autoResolveAuthorityAndTenant, DefaultAzureCredentialOptions credentialOptions = null, ILogger logger = null) + { + _credentialOptions = credentialOptions; + _autoResolveAuthorityAndTenant = autoResolveAuthorityAndTenant; + _logger = logger; + } + + /// + /// Returns the current access token for the connected ServiceClient instance + /// + /// + /// + public async Task GetAccessToken(string instanceUrl) + { + if (!Uri.IsWellFormedUriString(instanceUrl, UriKind.RelativeOrAbsolute)) + { + throw new ArgumentException("Invalid instance URL"); + } + AccessToken? accessToken = null; + Uri instanceUri = new Uri(instanceUrl); + if (_defaultAzureCredential == null) + { + Uri resourceUri = await InitializeCredentials(instanceUri).ConfigureAwait(false); + ResolveScopesList(instanceUri, resourceUri); + } + + // Get or create existing token. + _cacheList ??= new Dictionary(); + if (_cacheList.ContainsKey(instanceUri)) + { + accessToken = _cacheList[instanceUri]; + if (accessToken.HasValue && accessToken.Value.ExpiresOn < DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(30))) + accessToken = null; // flush the access token if it is about to expire. + } + + if ( accessToken == null) + { + Stopwatch sw = Stopwatch.StartNew(); + _logger.LogDebug("Getting new access token for {0}", instanceUri); + accessToken = await _defaultAzureCredential.GetTokenAsync(new Azure.Core.TokenRequestContext(ResolveScopesList(instanceUri))).ConfigureAwait(false); + _logger.LogDebug("Access token retrieved in {0}ms", sw.ElapsedMilliseconds); + sw.Stop(); + if(_cacheList.ContainsKey(instanceUri)) + { + _cacheList[instanceUri] = accessToken; + } + else + { + _cacheList.Add(instanceUri, accessToken); + } + } + + if (accessToken == null) + { + throw new Exception("Failed to retrieve access token"); + } + + return accessToken.Value.Token; + } + + private string[] ResolveScopesList(Uri instanceUrl , Uri resource = null) + { + _scopesList ??= new Dictionary>(); + if ( _scopesList.ContainsKey(instanceUrl)) + { + return _scopesList[instanceUrl].ToArray(); + } + if (resource == null) + { + throw new ArgumentNullException("Resource URI is required"); + } + else + { + _scopesList.Add(instanceUrl, new List { $"{resource}.default" }); + return _scopesList[instanceUrl].ToArray(); + } + } + + /// + /// Initialize the credentials for the current instance + /// + /// + /// + private async Task InitializeCredentials(Uri instanceUrl) + { + _logger.LogDebug("Initializing credentials for {0}", instanceUrl); + Stopwatch sw = Stopwatch.StartNew(); + + Uri resourceUri = null; + _credentialOptions ??= new DefaultAzureCredentialOptions(); + + if (_autoResolveAuthorityAndTenant) + { + _logger.LogDebug("Resolving authority and tenant for {0}", instanceUrl); + using var httpClient = new System.Net.Http.HttpClient(); + Auth.AuthorityResolver authorityResolver = new Auth.AuthorityResolver(httpClient); + var authDetails = await authorityResolver.ProbeForExpectedAuthentication(instanceUrl).ConfigureAwait(false); + resourceUri = authDetails.Resource; + _credentialOptions.AuthorityHost = authDetails.Authority; + _credentialOptions.TenantId = authDetails.Authority.Segments[1].Replace("/", ""); + + _logger.LogDebug("Authority and tenant resolved in {0}ms", sw.ElapsedMilliseconds); + _logger.LogDebug("Initialize Creds - found authority with name " + (string.IsNullOrEmpty(authDetails.Authority.ToString()) ? "" : authDetails.Authority.ToString())); + _logger.LogDebug("Initialize Creds - found resource with name " + (string.IsNullOrEmpty(authDetails.Resource.ToString()) ? "" : authDetails.Resource.ToString())); + _logger.LogDebug("Initialize Creds - found tenantId " + (string.IsNullOrEmpty(_credentialOptions.TenantId) ? "" : _credentialOptions.TenantId)); + } + _defaultAzureCredential = new DefaultAzureCredential(_credentialOptions); + + _logger.LogDebug("Credentials initialized in {0}ms", sw.ElapsedMilliseconds); + sw.Start(); + + return resourceUri; + } + + } +} diff --git a/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.Client.AzAuth/Microsoft.PowerPlatform.Dataverse.Client.AzAuth.csproj b/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.Client.AzAuth/Microsoft.PowerPlatform.Dataverse.Client.AzAuth.csproj new file mode 100644 index 0000000..fea7477 --- /dev/null +++ b/src/GeneralTools/DataverseClient/Extensions/Microsoft.PowerPlatform.Dataverse.Client.AzAuth/Microsoft.PowerPlatform.Dataverse.Client.AzAuth.csproj @@ -0,0 +1,26 @@ + + + + Microsoft.PowerPlatform.Dataverse.Client.AzAuth + DataverseClient + true + true + 8.0 + + + + + false + $(OutDir)\Microsoft.PowerPlatform.Dataverse.Client.AzAuth.xml + 6.0 + + + + + + + + + + + diff --git a/src/GeneralTools/DataverseClient/Testers/LoginControlTester/app.config b/src/GeneralTools/DataverseClient/Testers/LoginControlTester/app.config index 06ecd74..d861a6b 100644 --- a/src/GeneralTools/DataverseClient/Testers/LoginControlTester/app.config +++ b/src/GeneralTools/DataverseClient/Testers/LoginControlTester/app.config @@ -1,98 +1,104 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - + + + + + + + + - - + + - - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/AzAuthExtentionTests.cs b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/AzAuthExtentionTests.cs new file mode 100644 index 0000000..18f261d --- /dev/null +++ b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/AzAuthExtentionTests.cs @@ -0,0 +1,64 @@ +using Client_Core_UnitTests; +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.PowerPlatform.Dataverse.Client.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace DataverseClient_Core_UnitTests +{ + public class AzAuthExtentionTests + { + TestSupport testSupport = new TestSupport(); + public AzAuthExtentionTests(ITestOutputHelper output) + { + IConfiguration config = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .Build(); + + + + ILoggerFactory loggerFactory = LoggerFactory.Create(builder => + builder.AddConsole(options => + { + options.IncludeScopes = true; + options.TimestampFormat = "hh:mm:ss "; + }) + .AddConfiguration(config.GetSection("Logging")) + .AddProvider(new TraceConsoleLoggingProvider(output))); + testSupport.logger = loggerFactory.CreateLogger(); + } + + //[SkippableConnectionTest] + //[Fact] + [Trait("Category", "Live Connect Required")] + public void CreateServiceClient() + { + //var client = AzAuth.CreateServiceClient("", true); + //var client = AzAuth.CreateServiceClient("", true); + var client = AzAuth.CreateServiceClient(new ConnectionOptions() + { + ServiceUri = new Uri(""), + Logger = testSupport.logger + }); + + Assert.NotNull(client); + + var rslt = client.Execute(new WhoAmIRequest()); + Assert.NotNull(rslt); + Assert.IsType(rslt); + + var rlst1 = client.Execute(new RetrieveCurrentOrganizationRequest() { AccessType = 0}); + Assert.NotNull(rlst1); + Assert.IsType(rlst1); + + } + } +} diff --git a/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/DataverseClient_Core_UnitTests.csproj b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/DataverseClient_Core_UnitTests.csproj index 1df524e..9a52dc9 100644 --- a/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/DataverseClient_Core_UnitTests.csproj +++ b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/DataverseClient_Core_UnitTests.csproj @@ -25,6 +25,7 @@ + diff --git a/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/ServiceClientTests.cs b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/ServiceClientTests.cs index 98ab40f..71823ea 100644 --- a/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/ServiceClientTests.cs +++ b/src/GeneralTools/DataverseClient/UnitTests/CdsClient_Core_Tests/ServiceClientTests.cs @@ -1771,7 +1771,11 @@ public async void RequestBuilder_Execute() var trackingId = a.Id; - var rslt = await client.CreateRequestBuilder().WithCorrelationId(Guid.NewGuid()).WithHeader("User-Agent", "TEST").WithHeader("Foo", "TEST1").CreateAsync(a).ConfigureAwait(false); + var rslt = await client.CreateRequestBuilder() + .WithCorrelationId(Guid.NewGuid()) + .WithHeader("User-Agent", "TEST") + .WithHeader("Foo", "TEST1") + .CreateAsync(a).ConfigureAwait(false); Assert.IsType(rslt); a["name"] = "Test Account - step 2"; diff --git a/src/Packages.props b/src/Packages.props new file mode 100644 index 0000000..9692765 --- /dev/null +++ b/src/Packages.props @@ -0,0 +1,33 @@ + + + + 3.19.8 + 4.61.3 + 9.2.24044.9795-master + 9.2.24044.9795-master + 13.0.1 + 2.3.24 + 9.0.2.55 + 9.0.2.34 + 3.0.8 + 1.1.22 + 3.1.0 + 3.1.8 + 6.0.0 + 7.0.3 + 7.0.0 + 4.5.5 + 4.10.3 + 6.0.0 + 6.0.0 + 1.12.0 + + + 17.5.0 + 2.2.10 + 4.16.0 + 2.5.0 + 2.5.0 + 6.12.0 + + \ No newline at end of file diff --git a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.AzAuth.ReleaseNotes.txt b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.AzAuth.ReleaseNotes.txt new file mode 100644 index 0000000..75c97fc --- /dev/null +++ b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.AzAuth.ReleaseNotes.txt @@ -0,0 +1,9 @@ +Notice: + This package is an extension to the Microsoft.PowerPlatform.Dataverse.Client Nuget package. + This package is intended to work with .net full framework 4.6.2, 4.7.2 and 4.8, and .net 6.0 + +++CURRENTRELEASEID++ +Initial release +Provides an extension to the Dataverse ServiceClient to support authenticating with the Azure.Core DefaultAzureCredential flow. + + diff --git a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.AzAuth.nuspec b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.AzAuth.nuspec new file mode 100644 index 0000000..274ac43 --- /dev/null +++ b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.AzAuth.nuspec @@ -0,0 +1,46 @@ + + + + Microsoft.PowerPlatform.Dataverse.Client.AzAuth + 1.0.0 + Microsoft + crmsdk,Microsoft + https://go.microsoft.com/fwlink/?linkid=2108407 + https://github.com/microsoft/PowerPlatform-DataverseServiceClient + images\Dataverse.128x128.png + true + This package contains a auth extention for the Dataverse ServiceClient. This extention add support for authentication use the Azure.Core Library. This support the "DefaultAzureCredential" flow. This Package has been authored by the Microsoft Dataverse SDK team. + This package contains a auth extention for the Dataverse ServiceClient. This extention add support for authentication use the Azure.Core Library. + © Microsoft Corporation. All rights reserved. + Dynamics CommonDataService CDS PowerApps PowerPlatform ServiceClient Dataverse + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt index 24848c1..75bb23c 100644 --- a/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt +++ b/src/nuspecs/Microsoft.PowerPlatform.Dataverse.Client.ReleaseNotes.txt @@ -7,6 +7,16 @@ Notice: Note: Only AD on FullFramework, OAuth, Certificate, ClientSecret Authentication types are supported at this time. ++CURRENTRELEASEID++ +Fix for Logging MSAL telemetry when using ILogger + Previously, Logs for MSAL were not written to the configured ILogger, they would only go to Trace Source and InMemory Logs. +Fix for RequestBuilder to properly honor CrmUserId and AADOid in request builder requests. +Updated ServiceClient retry logic to use the server specified RetryAfter for Time and Concurrency throttling fault codes, in addition to Burst. +Updated ConnectionService retry logic to parse RetryAfter header as seconds instead of hours. +Dependency Changes: + Modified: + Microsoft.Identity.Client to 4.61.3 + +1.1.22: Fix for Retry hang introduced in 1.1.21: Dependency Changes: Added: