diff --git a/DynamicsCRMProvider.sln b/DynamicsCRMProvider.sln index a080c21..22b3a5d 100644 --- a/DynamicsCRMProvider.sln +++ b/DynamicsCRMProvider.sln @@ -21,6 +21,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{D383A6BC-F docs\tools\templates\template.cshtml = docs\tools\templates\template.cshtml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Crm.Services.Utility", "src\Microsoft.Crm.Services.Utility\Microsoft.Crm.Services.Utility.csproj", "{53CF97C2-34BC-4580-B62E-9F12E7626120}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,6 +33,10 @@ Global {4EEA1AD8-E8A9-4D54-BC39-D5F536366945}.Debug|Any CPU.Build.0 = Debug|Any CPU {4EEA1AD8-E8A9-4D54-BC39-D5F536366945}.Release|Any CPU.ActiveCfg = Release|Any CPU {4EEA1AD8-E8A9-4D54-BC39-D5F536366945}.Release|Any CPU.Build.0 = Release|Any CPU + {53CF97C2-34BC-4580-B62E-9F12E7626120}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53CF97C2-34BC-4580-B62E-9F12E7626120}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53CF97C2-34BC-4580-B62E-9F12E7626120}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53CF97C2-34BC-4580-B62E-9F12E7626120}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index b9f0451..fba9526 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,7 +1,8 @@ -#### 0.1.1 - 05.01.2015 +#### 0.2.0 - 08.02.2015 * Migration to Paket -* Latest version of ProvidedTypes.fs -* Reference API +* Update to the latest version of ProvidedTypes.fs +* Added dependency to Microsoft.CrmSdk.CoreAssemblies +* Added Reference API #### 0.1.0 - 26.01.2014 * Ported DynamicsCRMProvider from FSharpx \ No newline at end of file diff --git a/build.fsx b/build.fsx index bc60dc4..a1fe9ee 100644 --- a/build.fsx +++ b/build.fsx @@ -97,7 +97,7 @@ Target "AssemblyInfo" (fun _ -> |> Seq.iter (fun (projFileName, projectName, folderName, attributes) -> match projFileName with | Fsproj -> CreateFSharpAssemblyInfo (("src" @@ folderName) @@ "AssemblyInfo.fs") attributes - | Csproj -> CreateFSharpAssemblyInfo ((folderName @@ "Properties") @@ "AssemblyInfo.cs") attributes + | Csproj -> CreateCSharpAssemblyInfo ((folderName @@ "Properties") @@ "AssemblyInfo.cs") attributes | Vbproj -> CreateVisualBasicAssemblyInfo ((folderName @@ "My Project") @@ "AssemblyInfo.vb") attributes ) ) @@ -107,7 +107,7 @@ Target "AssemblyInfo" (fun _ -> // src folder to support multiple project outputs Target "CopyBinaries" (fun _ -> !! "src/**/*.??proj" - |> Seq.map (fun f -> ((System.IO.Path.GetDirectoryName f) @@ "bin/Release", "bin" @@ (System.IO.Path.GetFileNameWithoutExtension f))) + |> Seq.map (fun f -> ((System.IO.Path.GetDirectoryName f) @@ "bin/Release", "bin/DynamicsCRMProvider")) |> Seq.iter (fun (fromDir, toDir) -> CopyDir toDir fromDir (fun _ -> true)) ) diff --git a/paket.dependencies b/paket.dependencies index f0072f4..527e983 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -5,7 +5,7 @@ nuget NUnit nuget NUnit.Runners nuget FAKE nuget SourceLink.Fake -nuget DynamicsCRMProvider +nuget Microsoft.CrmSdk.CoreAssemblies < 6.0.0 github fsharp/FAKE modules/Octokit/Octokit.fsx github fsprojects/FSharp.TypeProviders.StarterPack src/ProvidedTypes.fsi diff --git a/paket.lock b/paket.lock index 1f0e775..5aed644 100644 --- a/paket.lock +++ b/paket.lock @@ -1,7 +1,6 @@ NUGET remote: https://nuget.org/api/v2 specs: - DynamicsCRMProvider (0.1.0) FAKE (3.18.0) FSharp.Compiler.Service (0.0.85) FSharp.Formatting (2.7.4) @@ -12,6 +11,9 @@ NUGET Microsoft.Bcl (1.1.10) Microsoft.Bcl.Build (>= 1.0.14) Microsoft.Bcl.Build (1.0.21) + Microsoft.CrmSdk.CoreAssemblies (5.0.18) + Microsoft.IdentityModel (>= 6.1.7600.16394) - framework: >= net40 + Microsoft.IdentityModel (6.1.7600.16394) - framework: >= net40 Microsoft.Net.Http (2.2.29) Microsoft.Bcl (>= 1.1.10) Microsoft.Bcl.Build (>= 1.0.14) diff --git a/src/DynamicsCRMProvider/AssemblyInfo.fs b/src/DynamicsCRMProvider/AssemblyInfo.fs index 3ffad52..69c576b 100644 --- a/src/DynamicsCRMProvider/AssemblyInfo.fs +++ b/src/DynamicsCRMProvider/AssemblyInfo.fs @@ -4,9 +4,9 @@ open System.Reflection [] [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = - let [] Version = "0.1.1" + let [] Version = "0.2.0" diff --git a/src/DynamicsCRMProvider/DynamicsCRMProvider.fsproj b/src/DynamicsCRMProvider/DynamicsCRMProvider.fsproj index 6b4f645..065d87d 100644 --- a/src/DynamicsCRMProvider/DynamicsCRMProvider.fsproj +++ b/src/DynamicsCRMProvider/DynamicsCRMProvider.fsproj @@ -60,6 +60,18 @@ True paket-files/ProvidedTypes.fs + + True + + + True + + + True + + + True + @@ -70,17 +82,12 @@ + False - - ..\..\packages\DynamicsCRMProvider\lib\net40\Microsoft.Crm.Services.Utility.dll - - - ..\..\packages\DynamicsCRMProvider\lib\net40\microsoft.xrm.sdk.dll - @@ -88,6 +95,13 @@ + + + Microsoft.Crm.Services.Utility + {53cf97c2-34bc-4580-b62e-9f12e7626120} + True + + + + + + + ..\..\packages\Microsoft.CrmSdk.CoreAssemblies\lib\net40\Microsoft.Crm.Sdk.Proxy.dll + True + True + + + ..\..\packages\Microsoft.CrmSdk.CoreAssemblies\lib\net40\Microsoft.Xrm.Sdk.dll + True + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + + + + + + + ..\..\packages\Microsoft.IdentityModel\lib\net35\Microsoft.IdentityModel.dll + True + True + + + + \ No newline at end of file diff --git a/src/DynamicsCRMProvider/paket.references b/src/DynamicsCRMProvider/paket.references index cb34510..38ec353 100644 --- a/src/DynamicsCRMProvider/paket.references +++ b/src/DynamicsCRMProvider/paket.references @@ -1,2 +1,3 @@ +Microsoft.CrmSdk.CoreAssemblies File:ProvidedTypes.fsi File:ProvidedTypes.fs \ No newline at end of file diff --git a/src/DynamicsCRMProvider/paket.template b/src/DynamicsCRMProvider/paket.template index d42347f..18563b7 100644 --- a/src/DynamicsCRMProvider/paket.template +++ b/src/DynamicsCRMProvider/paket.template @@ -23,6 +23,5 @@ description files ../../bin/DynamicsCRMProvider/FSharp.Data.DynamicsCRMProvider.pdb ==> lib/net40 ../../bin/DynamicsCRMProvider/FSharp.Data.DynamicsCRMProvider.xml ==> lib/net40 - ../../bin/DynamicsCRMProvider/Microsoft.Crm.Services.Utility.dll ==> lib/net40 - ../../bin/DynamicsCRMProvider/microsoft.xrm.sdk.xml ==> lib/net40 - ../../bin/DynamicsCRMProvider/microsoft.xrm.sdk.dll ==> lib/net40 \ No newline at end of file + ../../bin/DynamicsCRMProvider/Microsoft.Crm.Services.Utility.pdb ==> lib/net40 + ../../bin/DynamicsCRMProvider/Microsoft.Crm.Services.Utility.xml ==> lib/net40 diff --git a/src/Microsoft.Crm.Services.Utility/Microsoft.Crm.Services.Utility.csproj b/src/Microsoft.Crm.Services.Utility/Microsoft.Crm.Services.Utility.csproj new file mode 100644 index 0000000..84f7dad --- /dev/null +++ b/src/Microsoft.Crm.Services.Utility/Microsoft.Crm.Services.Utility.csproj @@ -0,0 +1,121 @@ + + + + + Debug + AnyCPU + {53CF97C2-34BC-4580-B62E-9F12E7626120} + Library + Properties + Microsoft.Crm.Services.Utility + Microsoft.Crm.Services.Utility + v4.0 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + bin\Release\Microsoft.Crm.Services.Utility.XML + + + + True + + + True + + + True + + + True + + + + + + + + + + + + + + + + + + + + + + + ..\..\packages\Microsoft.CrmSdk.CoreAssemblies\lib\net40\Microsoft.Crm.Sdk.Proxy.dll + True + True + + + ..\..\packages\Microsoft.CrmSdk.CoreAssemblies\lib\net40\Microsoft.Xrm.Sdk.dll + True + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + + + + + + + ..\..\packages\Microsoft.IdentityModel\lib\net35\Microsoft.IdentityModel.dll + True + True + + + + + \ No newline at end of file diff --git a/src/Microsoft.Crm.Services.Utility/Properties/AssemblyInfo.cs b/src/Microsoft.Crm.Services.Utility/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..e3fce41 --- /dev/null +++ b/src/Microsoft.Crm.Services.Utility/Properties/AssemblyInfo.cs @@ -0,0 +1,13 @@ +// +using System.Reflection; + +[assembly: AssemblyTitleAttribute("Microsoft.Crm.Services.Utility")] +[assembly: AssemblyProductAttribute("DynamicsCRMProvider")] +[assembly: AssemblyDescriptionAttribute("A type provider for Microsoft Dynamics CRM 2011.")] +[assembly: AssemblyVersionAttribute("0.2.0")] +[assembly: AssemblyFileVersionAttribute("0.2.0")] +namespace System { + internal static class AssemblyVersionInformation { + internal const string Version = "0.2.0"; + } +} diff --git a/src/Microsoft.Crm.Services.Utility/deviceidmanager.cs b/src/Microsoft.Crm.Services.Utility/deviceidmanager.cs new file mode 100644 index 0000000..d70dc8c --- /dev/null +++ b/src/Microsoft.Crm.Services.Utility/deviceidmanager.cs @@ -0,0 +1,995 @@ +// ===================================================================== +// +// This file is part of the Microsoft Dynamics CRM SDK code samples. +// +// Copyright (C) Microsoft Corporation. All rights reserved. +// +// This source code is intended only as a supplement to Microsoft +// Development Tools and/or on-line documentation. See these other +// materials for detailed information regarding Microsoft code samples. +// +// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY +// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A +// PARTICULAR PURPOSE. +// +// ===================================================================== +// +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Net; +using System.Runtime.Serialization; +using System.Security.Cryptography; +using System.ServiceModel.Description; +using System.Text; +using System.Xml; +using System.Xml.Serialization; + +namespace Microsoft.Crm.Services.Utility +{ + /// + /// Management utility for the Device Id + /// + public static class DeviceIdManager + { + #region Fields + private static readonly Random RandomInstance = new Random(); + + public const int MaxDeviceNameLength = 24; + public const int MaxDevicePasswordLength = 24; + #endregion + + #region Constructor + static DeviceIdManager() + { + PersistToFile = true; + } + #endregion + + #region Properties + /// + /// Indicates whether the registered device credentials should be persisted to the database + /// + public static bool PersistToFile { get; set; } + + /// + /// Indicates that the credentials should be persisted to the disk if registration fails with DeviceAlreadyExists. + /// + /// + /// If the device already exists, there is a possibility that the credentials are the same as the current credentials that + /// are being registered. This is especially true in automated environments where the same credentials are used continually (to avoid + /// registering spurious device credentials. + /// + public static bool PersistIfDeviceAlreadyExists { get; set; } + #endregion + + #region Methods + /// + /// Loads the device credentials (if they exist). + /// + /// + public static ClientCredentials LoadOrRegisterDevice() + { + return LoadOrRegisterDevice(null); + } + + /// + /// Loads the device credentials (if they exist). + /// + /// Device name that should be registered + /// Device password that should be registered + public static ClientCredentials LoadOrRegisterDevice(string deviceName, string devicePassword) + { + return LoadOrRegisterDevice(null, deviceName, devicePassword); + } + + /// + /// Loads the device credentials (if they exist). + /// + /// URL for the current token issuer + /// + /// The issuerUri can be retrieved from the IServiceConfiguration interface's CurrentIssuer property. + /// + public static ClientCredentials LoadOrRegisterDevice(Uri issuerUri) + { + return LoadOrRegisterDevice(issuerUri, null, null); + } + + /// + /// Loads the device credentials (if they exist). + /// + /// URL for the current token issuer + /// Device name that should be registered + /// Device password that should be registered + /// + /// The issuerUri can be retrieved from the IServiceConfiguration interface's CurrentIssuer property. + /// + public static ClientCredentials LoadOrRegisterDevice(Uri issuerUri, string deviceName, string devicePassword) + { + ClientCredentials credentials = LoadDeviceCredentials(issuerUri); + if (null == credentials) + { + credentials = RegisterDevice(Guid.NewGuid(), issuerUri, deviceName, devicePassword); + } + + return credentials; + } + + /// + /// Registers the given device with Microsoft account with a random application ID + /// + /// ClientCredentials that were registered + public static ClientCredentials RegisterDevice() + { + return RegisterDevice(Guid.NewGuid()); + } + + /// + /// Registers the given device with Microsoft account + /// + /// ID for the application + /// ClientCredentials that were registered + public static ClientCredentials RegisterDevice(Guid applicationId) + { + return RegisterDevice(applicationId, (Uri)null); + } + + /// + /// Registers the given device with Microsoft account + /// + /// ID for the application + /// URL for the current token issuer + /// ClientCredentials that were registered + /// + /// The issuerUri can be retrieved from the IServiceConfiguration interface's CurrentIssuer property. + /// + public static ClientCredentials RegisterDevice(Guid applicationId, Uri issuerUri) + { + return RegisterDevice(applicationId, issuerUri, null, null); + } + + /// + /// Registers the given device with Microsoft account + /// + /// ID for the application + /// Device name that should be registered + /// Device password that should be registered + /// ClientCredentials that were registered + public static ClientCredentials RegisterDevice(Guid applicationId, string deviceName, string devicePassword) + { + return RegisterDevice(applicationId, (Uri)null, deviceName, devicePassword); + } + + /// + /// Registers the given device with Microsoft account + /// + /// ID for the application + /// URL for the current token issuer + /// Device name that should be registered + /// Device password that should be registered + /// ClientCredentials that were registered + /// + /// The issuerUri can be retrieved from the IServiceConfiguration interface's CurrentIssuer property. + /// + public static ClientCredentials RegisterDevice(Guid applicationId, Uri issuerUri, string deviceName, string devicePassword) + { + if (string.IsNullOrEmpty(deviceName) && !PersistToFile) + { + throw new ArgumentNullException("deviceName", "If PersistToFile is false, then deviceName must be specified."); + } + else if (string.IsNullOrEmpty(deviceName) != string.IsNullOrEmpty(devicePassword)) + { + throw new ArgumentNullException("deviceName", "Either deviceName/devicePassword should both be specified or they should be null."); + } + + LiveDevice device = GenerateDevice(deviceName, devicePassword); + return RegisterDevice(applicationId, issuerUri, device); + } + + /// + /// Loads the device's credentials from the file system + /// + /// Device Credentials (if set) or null + public static ClientCredentials LoadDeviceCredentials() + { + return LoadDeviceCredentials(null); + } + + /// + /// Loads the device's credentials from the file system + /// + /// URL for the current token issuer + /// Device Credentials (if set) or null + /// + /// The issuerUri can be retrieved from the IServiceConfiguration interface's CurrentIssuer property. + /// + public static ClientCredentials LoadDeviceCredentials(Uri issuerUri) + { + //If the credentials should not be persisted to a file, then they won't be present on the disk. + if (!PersistToFile) + { + return null; + } + + EnvironmentConfiguration environment = DiscoverEnvironmentInternal(issuerUri); + + LiveDevice device = ReadExistingDevice(environment); + if (null == device || null == device.User) + { + return null; + } + + return device.User.ToClientCredentials(); + } + + /// + /// Discovers the Microsoft account environment based on the Token Issuer + /// + public static string DiscoverEnvironment(Uri issuerUri) + { + return DiscoverEnvironmentInternal(issuerUri).Environment; + } + #endregion + + #region Private Methods + private static EnvironmentConfiguration DiscoverEnvironmentInternal(Uri issuerUri) + { + if (null == issuerUri) + { + return new EnvironmentConfiguration(EnvironmentType.LiveDeviceID, "login.live.com", null); + } + + Dictionary searchList = new Dictionary(); + searchList.Add(EnvironmentType.LiveDeviceID, "login.live"); + searchList.Add(EnvironmentType.OrgDeviceID, "login.microsoftonline"); + + foreach (KeyValuePair searchPair in searchList) + { + if (issuerUri.Host.Length > searchPair.Value.Length && + issuerUri.Host.StartsWith(searchPair.Value, StringComparison.OrdinalIgnoreCase)) + { + string environment = issuerUri.Host.Substring(searchPair.Value.Length); + + //Parse out the environment + if ('-' == environment[0]) + { + int separatorIndex = environment.IndexOf('.', 1); + if (-1 != separatorIndex) + { + environment = environment.Substring(1, separatorIndex - 1); + } + else + { + environment = null; + } + } + else + { + environment = null; + } + + return new EnvironmentConfiguration(searchPair.Key, issuerUri.Host, environment); + } + } + + //In all other cases the environment is either not applicable or it is a production system + return new EnvironmentConfiguration(EnvironmentType.LiveDeviceID, issuerUri.Host, null); + } + + private static void Serialize(Stream stream, T value) + { + XmlSerializer serializer = new XmlSerializer(typeof(T), string.Empty); + + XmlSerializerNamespaces xmlNamespaces = new XmlSerializerNamespaces(); + xmlNamespaces.Add(string.Empty, string.Empty); + + serializer.Serialize(stream, value, xmlNamespaces); + } + + private static T Deserialize(string operationName, Stream stream) + { + //Read the XML into memory so that the data can be used in an exception if necessary + using (StreamReader reader = new StreamReader(stream)) + { + return Deserialize(operationName, reader.ReadToEnd()); + } + } + + private static T Deserialize(string operationName, string xml) + { + //Attempt to deserialize the data. If deserialization fails, include the XML in the exception that is thrown for further + //investigation + using (StringReader reader = new StringReader(xml)) + { + try + { + XmlSerializer serializer = new XmlSerializer(typeof(T), string.Empty); + return (T)serializer.Deserialize(reader); + } + catch (InvalidOperationException ex) + { + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, + "Unable to Deserialize XML (Operation = {0}):{1}{2}", operationName, Environment.NewLine, xml), ex); + } + } + } + + private static FileInfo GetDeviceFile(EnvironmentConfiguration environment) + { + return new FileInfo(string.Format(CultureInfo.InvariantCulture, LiveIdConstants.FileNameFormat, + environment.Type, + string.IsNullOrEmpty(environment.Environment) ? null : "-" + environment.Environment.ToUpperInvariant())); + } + + private static ClientCredentials RegisterDevice(Guid applicationId, Uri issuerUri, LiveDevice device) + { + EnvironmentConfiguration environment = DiscoverEnvironmentInternal(issuerUri); + + DeviceRegistrationRequest request = new DeviceRegistrationRequest(applicationId, device); + + string url = string.Format(CultureInfo.InvariantCulture, LiveIdConstants.RegistrationEndpointUriFormat, + environment.HostName); + + DeviceRegistrationResponse response = ExecuteRegistrationRequest(url, request); + if (!response.IsSuccess) + { + bool throwException = true; + if (DeviceRegistrationErrorCode.DeviceAlreadyExists == response.Error.RegistrationErrorCode) + { + if (!PersistToFile) + { + //If the file is not persisted, the registration will always occur (since the credentials are not + //persisted to the disk. However, the credentials may already exist. To avoid an exception being continually + //processed by the calling user, DeviceAlreadyExists will be ignored if the credentials are not persisted to the disk. + return device.User.ToClientCredentials(); + } + else if (PersistIfDeviceAlreadyExists) + { + // This flag indicates that the + throwException = false; + } + } + + if (throwException) + { + throw new DeviceRegistrationFailedException(response.Error.RegistrationErrorCode, response.ErrorSubCode); + } + } + + if (PersistToFile || PersistIfDeviceAlreadyExists) + { + WriteDevice(environment, device); + } + + return device.User.ToClientCredentials(); + } + + private static LiveDevice GenerateDevice(string deviceName, string devicePassword) + { + // If the deviceName hasn't been specified, it should be generated using random characters. + DeviceUserName userNameCredentials; + if (string.IsNullOrEmpty(deviceName)) + { + userNameCredentials = GenerateDeviceUserName(); + } + else + { + userNameCredentials = new DeviceUserName() { DeviceName = deviceName, DecryptedPassword = devicePassword }; + } + + return new LiveDevice() { User = userNameCredentials, Version = 1 }; + } + + private static LiveDevice ReadExistingDevice(EnvironmentConfiguration environment) + { + //Retrieve the file info + FileInfo file = GetDeviceFile(environment); + if (!file.Exists) + { + return null; + } + + using (FileStream stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.Read)) + { + return Deserialize("Loading Device Credentials from Disk", stream); + } + } + + private static void WriteDevice(EnvironmentConfiguration environment, LiveDevice device) + { + FileInfo file = GetDeviceFile(environment); + if (!file.Directory.Exists) + { + file.Directory.Create(); + } + + using (FileStream stream = file.Open(FileMode.CreateNew, FileAccess.Write, FileShare.None)) + { + Serialize(stream, device); + } + } + + private static DeviceRegistrationResponse ExecuteRegistrationRequest(string url, DeviceRegistrationRequest registrationRequest) + { + //Create the request that will submit the request to the server + WebRequest request = WebRequest.Create(url); + request.ContentType = "application/soap+xml; charset=UTF-8"; + request.Method = "POST"; + request.Timeout = 180000; + + //Write the envelope to the RequestStream + using (Stream stream = request.GetRequestStream()) + { + Serialize(stream, registrationRequest); + } + + // Read the response into an XmlDocument and return that doc + try + { + using (WebResponse response = request.GetResponse()) + { + using (Stream stream = response.GetResponseStream()) + { + return Deserialize("Deserializing Registration Response", stream); + } + } + } + catch (WebException ex) + { + System.Diagnostics.Trace.TraceError("Microsoft account Device Registration Failed (HTTP Code: {0}): {1}", + ex.Status, ex.Message); + + if (null != ex.Response) + { + using (Stream stream = ex.Response.GetResponseStream()) + { + return Deserialize("Deserializing Failed Registration Response", stream); + } + } + + throw; + } + } + + private static DeviceUserName GenerateDeviceUserName() + { + DeviceUserName userName = new DeviceUserName(); + userName.DeviceName = GenerateRandomString(LiveIdConstants.ValidDeviceNameCharacters, MaxDeviceNameLength); + userName.DecryptedPassword = GenerateRandomString(LiveIdConstants.ValidDevicePasswordCharacters, MaxDevicePasswordLength); + + return userName; + } + + private static string GenerateRandomString(string characterSet, int count) + { + //Create an array of the characters that will hold the final list of random characters + char[] value = new char[count]; + + //Convert the character set to an array that can be randomly accessed + char[] set = characterSet.ToCharArray(); + + lock (RandomInstance) + { + //Populate the array with random characters from the character set + for (int i = 0; i < count; i++) + { + value[i] = set[RandomInstance.Next(0, set.Length)]; + } + } + + return new string(value); + } + #endregion + + #region Private Classes + private enum EnvironmentType + { + LiveDeviceID, + OrgDeviceID + } + + private sealed class EnvironmentConfiguration + { + public EnvironmentConfiguration(EnvironmentType type, string hostName, string environment) + { + if (string.IsNullOrWhiteSpace(hostName)) + { + throw new ArgumentNullException("hostName"); + } + + this.Type = type; + this.HostName = hostName; + this.Environment = environment; + } + + #region Properties + public EnvironmentType Type { get; private set; } + + public string HostName { get; private set; } + + public string Environment { get; private set; } + #endregion + } + + private static class LiveIdConstants + { + public const string RegistrationEndpointUriFormat = @"https://{0}/ppsecure/DeviceAddCredential.srf"; + + public static readonly string FileNameFormat = Path.Combine( + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "LiveDeviceID"), + "{0}{1}.xml"); + + public const string ValidDeviceNameCharacters = "0123456789abcdefghijklmnopqrstuvqxyz"; + + //Consists of the list of characters specified in the documentation + public const string ValidDevicePasswordCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^*()-_=+;,./?`~"; + } + #endregion + } + + #region Public Classes & Enums + /// + /// Indicates an error during registration + /// + public enum DeviceRegistrationErrorCode + { + /// + /// Unspecified or Unknown Error occurred + /// + Unknown = 0, + + /// + /// Interface Disabled + /// + InterfaceDisabled = 1, + + /// + /// Invalid Request Format + /// + InvalidRequestFormat = 3, + + /// + /// Unknown Client Version + /// + UnknownClientVersion = 4, + + /// + /// Blank Password + /// + BlankPassword = 6, + + /// + /// Missing Device User Name or Password + /// + MissingDeviceUserNameOrPassword = 7, + + /// + /// Invalid Parameter Syntax + /// + InvalidParameterSyntax = 8, + + /// + /// Invalid Characters are used in the device credentials. + /// + InvalidCharactersInCredentials = 9, + + /// + /// Internal Error + /// + InternalError = 11, + + /// + /// Device Already Exists + /// + DeviceAlreadyExists = 13 + } + + /// + /// Indicates that Device Registration failed + /// + [Serializable] + public sealed class DeviceRegistrationFailedException : Exception + { + /// + /// Construct an instance of the DeviceRegistrationFailedException class + /// + public DeviceRegistrationFailedException() + : base() + { + } + + /// + /// Construct an instance of the DeviceRegistrationFailedException class + /// + /// Message to pass + public DeviceRegistrationFailedException(string message) + : base(message) + { + } + + /// + /// Construct an instance of the DeviceRegistrationFailedException class + /// + /// Message to pass + /// Exception to include + public DeviceRegistrationFailedException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Construct an instance of the DeviceRegistrationFailedException class + /// + /// Error code that occurred + /// Subcode that occurred + public DeviceRegistrationFailedException(DeviceRegistrationErrorCode code, string subCode) + : this(code, subCode, null) + { + } + + /// + /// Construct an instance of the DeviceRegistrationFailedException class + /// + /// Error code that occurred + /// Subcode that occurred + /// Inner exception + public DeviceRegistrationFailedException(DeviceRegistrationErrorCode code, string subCode, Exception innerException) + : base(string.Concat(code.ToString(), ": ", subCode), innerException) + { + this.RegistrationErrorCode = code; + } + + /// + /// Construct an instance of the DeviceRegistrationFailedException class + /// + /// + /// + private DeviceRegistrationFailedException(SerializationInfo si, StreamingContext sc) + : base(si, sc) + { + } + + #region Properties + /// + /// Error code that occurred during registration + /// + public DeviceRegistrationErrorCode RegistrationErrorCode { get; private set; } + #endregion + + #region Methods + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + } + #endregion + } + + #region Serialization Classes + #region DeviceRegistrationRequest Class + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlRoot("DeviceAddRequest")] + public sealed class DeviceRegistrationRequest + { + #region Constructors + public DeviceRegistrationRequest() + { + } + + public DeviceRegistrationRequest(Guid applicationId, LiveDevice device) + : this() + { + if (null == device) + { + throw new ArgumentNullException("device"); + } + + this.ClientInfo = new DeviceRegistrationClientInfo() { ApplicationId = applicationId, Version = "1.0" }; + this.Authentication = new DeviceRegistrationAuthentication() + { + MemberName = device.User.DeviceId, + Password = device.User.DecryptedPassword + }; + } + #endregion + + #region Properties + [XmlElement("ClientInfo")] + public DeviceRegistrationClientInfo ClientInfo { get; set; } + + [XmlElement("Authentication")] + public DeviceRegistrationAuthentication Authentication { get; set; } + #endregion + } + #endregion + + #region DeviceRegistrationClientInfo Class + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlRoot("ClientInfo")] + public sealed class DeviceRegistrationClientInfo + { + #region Properties + [XmlAttribute("name")] + public Guid ApplicationId { get; set; } + + [XmlAttribute("version")] + public string Version { get; set; } + #endregion + } + #endregion + + #region DeviceRegistrationAuthentication Class + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlRoot("Authentication")] + public sealed class DeviceRegistrationAuthentication + { + #region Properties + [XmlElement("Membername")] + public string MemberName { get; set; } + + [XmlElement("Password")] + public string Password { get; set; } + #endregion + } + #endregion + + #region DeviceRegistrationResponse Class + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlRoot("DeviceAddResponse")] + public sealed class DeviceRegistrationResponse + { + #region Properties + [XmlElement("success")] + public bool IsSuccess { get; set; } + + [XmlElement("puid")] + public string Puid { get; set; } + + [XmlElement("Error")] + public DeviceRegistrationResponseError Error { get; set; } + + [XmlElement("ErrorSubcode")] + public string ErrorSubCode { get; set; } + #endregion + } + #endregion + + #region DeviceRegistrationResponse Class + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlRoot("Error")] + public sealed class DeviceRegistrationResponseError + { + private string _code; + + #region Properties + [XmlAttribute("Code")] + public string Code + { + get + { + return this._code; + } + + set + { + this._code = value; + + //Parse the error code + if (!string.IsNullOrEmpty(value)) + { + //Parse the error code + if (value.StartsWith("dc", StringComparison.Ordinal)) + { + int code; + if (int.TryParse(value.Substring(2), NumberStyles.Integer, + CultureInfo.InvariantCulture, out code) && + Enum.IsDefined(typeof(DeviceRegistrationErrorCode), code)) + { + this.RegistrationErrorCode = (DeviceRegistrationErrorCode)Enum.ToObject( + typeof(DeviceRegistrationErrorCode), code); + } + } + } + } + } + + [XmlIgnore] + public DeviceRegistrationErrorCode RegistrationErrorCode { get; private set; } + #endregion + } + #endregion + + #region LiveDevice Class + [EditorBrowsable(EditorBrowsableState.Never)] + [XmlRoot("Data")] + public sealed class LiveDevice + { + #region Properties + [XmlAttribute("version")] + public int Version { get; set; } + + [XmlElement("User")] + public DeviceUserName User { get; set; } + + [SuppressMessage("Microsoft.Design", "CA1059:MembersShouldNotExposeCertainConcreteTypes", MessageId = "System.Xml.XmlNode", Justification = "This is required for proper XML Serialization")] + [XmlElement("Token")] + public XmlNode Token { get; set; } + + [XmlElement("Expiry")] + public string Expiry { get; set; } + + [XmlElement("ClockSkew")] + public string ClockSkew { get; set; } + #endregion + } + #endregion + + #region DeviceUserName Class + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class DeviceUserName + { + private string _encryptedPassword; + private string _decryptedPassword; + private bool _encryptedValueIsUpdated; + + #region Constants + private const string UserNamePrefix = "11"; + #endregion + + #region Constructors + public DeviceUserName() + { + this.UserNameType = "Logical"; + } + #endregion + + #region Properties + [XmlAttribute("username")] + public string DeviceName { get; set; } + + [XmlAttribute("type")] + public string UserNameType { get; set; } + + [XmlElement("Pwd")] + public string EncryptedPassword + { + get + { + this.ThrowIfNoEncryption(); + + if (!this._encryptedValueIsUpdated) + { + this._encryptedPassword = this.Encrypt(this._decryptedPassword); + this._encryptedValueIsUpdated = true; + } + + return this._encryptedPassword; + } + + set + { + this.ThrowIfNoEncryption(); + this.UpdateCredentials(value, null); + } + } + + public string DeviceId + { + get + { + return UserNamePrefix + DeviceName; + } + } + + [XmlIgnore] + public string DecryptedPassword + { + get + { + return this._decryptedPassword; + } + + set + { + this.UpdateCredentials(null, value); + } + } + + private bool IsEncryptionEnabled + { + get + { + //If the object is not going to be persisted to a file, then the value does not need to be encrypted. This is extra + //overhead and will not function in partial trust. + return DeviceIdManager.PersistToFile; + } + } + #endregion + + #region Methods + public ClientCredentials ToClientCredentials() + { + ClientCredentials credentials = new ClientCredentials(); + credentials.UserName.UserName = this.DeviceId; + credentials.UserName.Password = this.DecryptedPassword; + + return credentials; + } + + private void ThrowIfNoEncryption() + { + if (!this.IsEncryptionEnabled) + { + throw new NotSupportedException("Not supported when DeviceIdManager.UseEncryptionApis is false."); + } + } + + private void UpdateCredentials(string encryptedValue, string decryptedValue) + { + bool isValueUpdated = false; + if (string.IsNullOrEmpty(encryptedValue) && string.IsNullOrEmpty(decryptedValue)) + { + isValueUpdated = true; + } + else if (string.IsNullOrEmpty(encryptedValue)) + { + if (this.IsEncryptionEnabled) + { + encryptedValue = this.Encrypt(decryptedValue); + isValueUpdated = true; + } + else + { + encryptedValue = null; + isValueUpdated = false; + } + } + else + { + this.ThrowIfNoEncryption(); + + decryptedValue = this.Decrypt(encryptedValue); + isValueUpdated = true; + } + + this._encryptedPassword = encryptedValue; + this._decryptedPassword = decryptedValue; + this._encryptedValueIsUpdated = isValueUpdated; + } + + private string Encrypt(string value) + { + if (string.IsNullOrEmpty(value)) + { + return value; + } + + byte[] encryptedBytes = ProtectedData.Protect(Encoding.UTF8.GetBytes(value), null, DataProtectionScope.CurrentUser); + return Convert.ToBase64String(encryptedBytes); + } + + private string Decrypt(string value) + { + if (string.IsNullOrEmpty(value)) + { + return value; + } + + byte[] decryptedBytes = ProtectedData.Unprotect(Convert.FromBase64String(value), null, DataProtectionScope.CurrentUser); + if (null == decryptedBytes || 0 == decryptedBytes.Length) + { + return null; + } + + return Encoding.UTF8.GetString(decryptedBytes, 0, decryptedBytes.Length); + } + #endregion + } + #endregion + #endregion + #endregion +} +// diff --git a/src/Microsoft.Crm.Services.Utility/paket.references b/src/Microsoft.Crm.Services.Utility/paket.references new file mode 100644 index 0000000..08db74d --- /dev/null +++ b/src/Microsoft.Crm.Services.Utility/paket.references @@ -0,0 +1 @@ +Microsoft.CrmSdk.CoreAssemblies \ No newline at end of file