User management support classes (#1856)

* WIP

* Fix nullable warnings in UserData class

* WIP2

* WIP

* Refresh database prototype classes

* Fix some typos & merge issues

* WIP

* WIP

* WIP

* Additional updates

* Remove unneded using
This commit is contained in:
Karl Burtram
2023-02-08 18:02:08 -08:00
committed by GitHub
parent ee086e2067
commit 2ef5f0918a
24 changed files with 3584 additions and 1336 deletions

View File

@@ -58,7 +58,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts
/// </summary>
public class UserInfo
{
DatabaseUserType? Type { get; set; }
public DatabaseUserType? Type { get; set; }
public string UserName { get; set; }
public string LoginName { get; set; }
@@ -72,9 +74,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts
public bool isAAD { get; set; }
public ExtendedProperty[] ExtendedProperties { get; set; }
public ExtendedProperty[]? ExtendedProperties { get; set; }
public SecurablePermissions[] SecurablePermissions { get; set; }
public SecurablePermissions[]? SecurablePermissions { get; set; }
}
}

View File

@@ -0,0 +1,66 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts
{
/// <summary>
/// Create User parameters
/// </summary>
public class CreateUserParams : GeneralRequestDetails
{
public string OwnerUri { get; set; }
public UserInfo User { get; set; }
}
/// <summary>
/// Create User result
/// </summary>
public class CreateUserResult : ResultStatus
{
public UserInfo User { get; set; }
}
/// <summary>
/// Create User request type
/// </summary>
public class CreateUserRequest
{
/// <summary>
/// Request definition
/// </summary>
public static readonly
RequestType<CreateUserParams, CreateUserResult> Type =
RequestType<CreateUserParams, CreateUserResult>.Create("objectmanagement/createuser");
}
/// <summary>
/// Delete User params
/// </summary>
public class DeleteUserParams : GeneralRequestDetails
{
public string OwnerUri { get; set; }
public string UserName { get; set; }
}
/// <summary>
/// Delete User request type
/// </summary>
public class DeleteUserRequest
{
/// <summary>
/// Request definition
/// </summary>
public static readonly
RequestType<DeleteUserParams, ResultStatus> Type =
RequestType<DeleteUserParams, ResultStatus>.Create("objectmanagement/deleteuser");
}
}

View File

@@ -9,8 +9,8 @@ using System;
using System.Collections;
using System.Collections.Specialized;
using System.Data;
using System.Security;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.Dmf;
using Microsoft.SqlServer.Management.Sdk.Sfc;
@@ -31,7 +31,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
{
private bool disposed;
private ConnectionService connectionService = null;
private ConnectionService connectionService;
private static readonly Lazy<SecurityService> instance = new Lazy<SecurityService>(() => new SecurityService());
@@ -93,6 +93,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
// Login request handlers
this.ServiceHost.SetRequestHandler(CreateLoginRequest.Type, HandleCreateLoginRequest, true);
this.ServiceHost.SetRequestHandler(DeleteLoginRequest.Type, HandleDeleteLoginRequest, true);
// User request handlers
this.ServiceHost.SetRequestHandler(CreateUserRequest.Type, HandleCreateUserRequest, true);
}
@@ -170,112 +173,73 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
#region "User Handlers"
private UserPrototype InitUserNew(CDataContainer dataContainer)
internal Task<Tuple<bool, string>> ConfigureUser(
string ownerUri,
UserInfo user,
ConfigAction configAction,
RunType runType)
{
// this.DataContainer = context;
// this.parentDbUrn = new Urn(this.DataContainer.ParentUrn);
// this.objectUrn = new Urn(this.DataContainer.ObjectUrn);
ExhaustiveUserTypes currentUserType;
UserPrototypeFactory userPrototypeFactory = UserPrototypeFactory.GetInstance(dataContainer);
if (dataContainer.IsNewObject)
return Task<Tuple<bool, string>>.Run(() =>
{
if (IsParentDatabaseContained(dataContainer.ParentUrn, dataContainer))
try
{
currentUserType = ExhaustiveUserTypes.SqlUserWithPassword;
}
else
{
currentUserType = ExhaustiveUserTypes.LoginMappedUser;
}
}
else
{
currentUserType = this.GetCurrentUserTypeForExistingUser(
dataContainer.Server.GetSmoObject(dataContainer.ObjectUrn) as User);
}
UserPrototype currentUserPrototype = userPrototypeFactory.GetUserPrototype(currentUserType);
return currentUserPrototype;
}
private ExhaustiveUserTypes GetCurrentUserTypeForExistingUser(User user)
{
switch (user.UserType)
{
case UserType.SqlUser:
if (user.IsSupportedProperty("AuthenticationType"))
ConnectionInfo connInfo;
ConnectionServiceInstance.TryFindConnection(ownerUri, out connInfo);
if (connInfo == null)
{
if (user.AuthenticationType == AuthenticationType.Windows)
{
return ExhaustiveUserTypes.WindowsUser;
}
else if (user.AuthenticationType == AuthenticationType.Database)
{
return ExhaustiveUserTypes.SqlUserWithPassword;
}
throw new ArgumentException("Invalid connection URI '{0}'", ownerUri);
}
return ExhaustiveUserTypes.LoginMappedUser;
case UserType.NoLogin:
return ExhaustiveUserTypes.SqlUserWithoutLogin;
case UserType.Certificate:
return ExhaustiveUserTypes.CertificateMappedUser;
case UserType.AsymmetricKey:
return ExhaustiveUserTypes.AsymmetricKeyMappedUser;
default:
return ExhaustiveUserTypes.Unknown;
}
var serverConnection = ConnectionService.OpenServerConnection(connInfo, "DataContainer");
var connectionInfoWithConnection = new SqlConnectionInfoWithConnection();
connectionInfoWithConnection.ServerConnection = serverConnection;
string urn = string.Format(System.Globalization.CultureInfo.InvariantCulture,
"Server/Database[@Name='{0}']",
Urn.EscapeString(serverConnection.DatabaseName));
ActionContext context = new ActionContext(serverConnection, "new_user", urn);
DataContainerXmlGenerator containerXml = new DataContainerXmlGenerator(context);
containerXml.AddProperty("itemtype", "User");
XmlDocument xmlDoc = containerXml.GenerateXmlDocument();
bool objectExists = configAction != ConfigAction.Create;
CDataContainer dataContainer = CDataContainer.CreateDataContainer(connectionInfoWithConnection, xmlDoc);
using (var actions = new UserActions(dataContainer, user, configAction))
{
var executionHandler = new ExecutonHandler(actions);
executionHandler.RunNow(runType, this);
}
return new Tuple<bool, string>(true, string.Empty);
}
catch (Exception ex)
{
return new Tuple<bool, string>(false, ex.ToString());
}
});
}
private bool IsParentDatabaseContained(Urn parentDbUrn, CDataContainer dataContainer)
/// <summary>
/// Handle request to create a user
/// </summary>
internal async Task HandleCreateUserRequest(CreateUserParams parameters, RequestContext<CreateUserResult> requestContext)
{
string parentDbName = parentDbUrn.GetNameForType("Database");
Database parentDatabase = dataContainer.Server.Databases[parentDbName];
var result = await ConfigureUser(parameters.OwnerUri,
parameters.User,
ConfigAction.Create,
RunType.RunNow);
if (parentDatabase.IsSupportedProperty("ContainmentType")
&& parentDatabase.ContainmentType == ContainmentType.Partial)
await requestContext.SendResult(new CreateUserResult()
{
return true;
}
return false;
User = parameters.User,
Success = result.Item1,
ErrorMessage = result.Item2
});
}
private void GetUserTypeOptions(CDataContainer dataContainer)
{
if (SqlMgmtUtils.IsSql11OrLater(dataContainer.Server.ServerVersion)
&& IsParentDatabaseContained(dataContainer.ParentUrn, dataContainer))
{
// this.userTypeComboBox.Items.AddRange(
// new string[]{
// UserSR.SqlUserWithPasswordUserTypeText
// }
//);
}
if (SqlMgmtUtils.IsYukonOrAbove(dataContainer.Server))
{
// this.userTypeComboBox.Items.AddRange(
// new string[]{
// UserSR.AsymmetricKeyUserTypeText,
// UserSR.CertificateUserTypeText,
// UserSR.WithoutLoginSqlUserTypeText,
// UserSR.WindowsUserTypeText
// }
// );
}
// this.userTypeComboBox.Items.AddRange(
// new string[]{
// UserSR.LoginMappedSqlUserTypeText
// }
// );
}
private void GetDefaultLanguageOptions(CDataContainer dataContainer)
{
// this.defaultLanguageComboBox.Items.Clear();
@@ -298,65 +262,54 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
}
}
private SecureString GetReadOnlySecureString(string secret)
{
SecureString ss = new SecureString();
foreach (char c in secret.ToCharArray())
{
ss.AppendChar(c);
}
ss.MakeReadOnly();
// code needs to be ported into the useraction class
// public void UserMemberships_OnRunNow(object sender, CDataContainer dataContainer)
// {
// UserPrototype currentPrototype = UserPrototypeFactory.GetInstance(dataContainer).CurrentPrototype;
return ss;
}
// //In case the UserGeneral/OwnedSchemas pages are loaded,
// //those will takes care of applying membership changes also.
// //Hence, we only need to apply changes in this method when those are not loaded.
// if (!currentPrototype.IsRoleMembershipChangesApplied)
// {
// //base.OnRunNow(sender);
public void UserMemberships_OnRunNow(object sender, CDataContainer dataContainer)
{
UserPrototype currentPrototype = UserPrototypeFactory.GetInstance(dataContainer).CurrentPrototype;
// User user = currentPrototype.ApplyChanges();
//In case the UserGeneral/OwnedSchemas pages are loaded,
//those will takes care of applying membership changes also.
//Hence, we only need to apply changes in this method when those are not loaded.
if (!currentPrototype.IsRoleMembershipChangesApplied)
{
//base.OnRunNow(sender);
// //this.ExecutionMode = ExecutionMode.Success;
// dataContainer.ObjectName = currentPrototype.Name;
// dataContainer.SqlDialogSubject = user;
// }
User user = currentPrototype.ApplyChanges();
// //setting back to original after changes are applied
// currentPrototype.IsRoleMembershipChangesApplied = false;
// }
//this.ExecutionMode = ExecutionMode.Success;
dataContainer.ObjectName = currentPrototype.Name;
dataContainer.SqlDialogSubject = user;
}
// /// <summary>
// /// implementation of OnPanelRunNow
// /// </summary>
// /// <param name="node"></param>
// public void UserOwnedSchemas_OnRunNow(object sender, CDataContainer dataContainer)
// {
// UserPrototype currentPrototype = UserPrototypeFactory.GetInstance(dataContainer).CurrentPrototype;
//setting back to original after changes are applied
currentPrototype.IsRoleMembershipChangesApplied = false;
}
// //In case the UserGeneral/Membership pages are loaded,
// //those will takes care of applying schema ownership changes also.
// //Hence, we only need to apply changes in this method when those are not loaded.
// if (!currentPrototype.IsSchemaOwnershipChangesApplied)
// {
// //base.OnRunNow(sender);
/// <summary>
/// implementation of OnPanelRunNow
/// </summary>
/// <param name="node"></param>
public void UserOwnedSchemas_OnRunNow(object sender, CDataContainer dataContainer)
{
UserPrototype currentPrototype = UserPrototypeFactory.GetInstance(dataContainer).CurrentPrototype;
// User user = currentPrototype.ApplyChanges();
//In case the UserGeneral/Membership pages are loaded,
//those will takes care of applying schema ownership changes also.
//Hence, we only need to apply changes in this method when those are not loaded.
if (!currentPrototype.IsSchemaOwnershipChangesApplied)
{
//base.OnRunNow(sender);
// //this.ExecutionMode = ExecutionMode.Success;
// dataContainer.ObjectName = currentPrototype.Name;
// dataContainer.SqlDialogSubject = user;
// }
User user = currentPrototype.ApplyChanges();
//this.ExecutionMode = ExecutionMode.Success;
dataContainer.ObjectName = currentPrototype.Name;
dataContainer.SqlDialogSubject = user;
}
//setting back to original after changes are applied
currentPrototype.IsSchemaOwnershipChangesApplied = false;
}
// //setting back to original after changes are applied
// currentPrototype.IsSchemaOwnershipChangesApplied = false;
// }
// how to populate defaults from prototype, will delete once refactored
// private void InitializeValuesInUiControls()
@@ -855,7 +808,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
Database database = dataContainer.Server.Databases[databaseName];
System.Diagnostics.Debug.Assert(database!= null, "database is null");
DatabaseRole role = null;
DatabaseRole role;
if (isPropertiesMode == true) // in properties mode -> alter role
{
@@ -888,7 +841,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
private void DbRole_LoadSchemas(string databaseName, string dbroleName, ServerConnection serverConnection)
{
bool isPropertiesMode = false;
HybridDictionary schemaOwnership = null;
HybridDictionary schemaOwnership;
schemaOwnership = new HybridDictionary();
Enumerator en = new Enumerator();

View File

@@ -0,0 +1,144 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlServer.Management.Sdk.Sfc;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlTools.ServiceLayer.Management;
using Microsoft.SqlTools.ServiceLayer.Security.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Security
{
internal class UserActions : ManagementActionBase
{
#region Variables
//private UserPrototypeData userData;
private UserPrototype userPrototype;
private UserInfo user;
private ConfigAction configAction;
#endregion
#region Constructors / Dispose
/// <summary>
/// required when loading from Object Explorer context
/// </summary>
/// <param name="context"></param>
public UserActions(
CDataContainer context,
UserInfo user,
ConfigAction configAction)
{
this.DataContainer = context;
this.user = user;
this.configAction = configAction;
this.userPrototype = InitUserNew(context, user);
}
// /// <summary>
// /// Clean up any resources being used.
// /// </summary>
// protected override void Dispose(bool disposing)
// {
// base.Dispose(disposing);
// }
#endregion
/// <summary>
/// called on background thread by the framework to execute the action
/// </summary>
/// <param name="node"></param>
public override void OnRunNow(object sender)
{
if (this.configAction == ConfigAction.Drop)
{
// if (this.credentialData.Credential != null)
// {
// this.credentialData.Credential.DropIfExists();
// }
}
else
{
this.userPrototype.ApplyChanges();
}
}
private UserPrototype InitUserNew(CDataContainer dataContainer, UserInfo user)
{
// this.DataContainer = context;
// this.parentDbUrn = new Urn(this.DataContainer.ParentUrn);
// this.objectUrn = new Urn(this.DataContainer.ObjectUrn);
ExhaustiveUserTypes currentUserType;
UserPrototypeFactory userPrototypeFactory = UserPrototypeFactory.GetInstance(dataContainer, user);
if (dataContainer.IsNewObject)
{
if (IsParentDatabaseContained(dataContainer.ParentUrn, dataContainer))
{
currentUserType = ExhaustiveUserTypes.SqlUserWithPassword;
}
else
{
currentUserType = ExhaustiveUserTypes.LoginMappedUser;
}
}
else
{
currentUserType = this.GetCurrentUserTypeForExistingUser(
dataContainer.Server.GetSmoObject(dataContainer.ObjectUrn) as User);
}
UserPrototype currentUserPrototype = userPrototypeFactory.GetUserPrototype(currentUserType);
return currentUserPrototype;
}
private ExhaustiveUserTypes GetCurrentUserTypeForExistingUser(User user)
{
switch (user.UserType)
{
case UserType.SqlUser:
if (user.IsSupportedProperty("AuthenticationType"))
{
if (user.AuthenticationType == AuthenticationType.Windows)
{
return ExhaustiveUserTypes.WindowsUser;
}
else if (user.AuthenticationType == AuthenticationType.Database)
{
return ExhaustiveUserTypes.SqlUserWithPassword;
}
}
return ExhaustiveUserTypes.LoginMappedUser;
case UserType.NoLogin:
return ExhaustiveUserTypes.SqlUserWithoutLogin;
case UserType.Certificate:
return ExhaustiveUserTypes.CertificateMappedUser;
case UserType.AsymmetricKey:
return ExhaustiveUserTypes.AsymmetricKeyMappedUser;
default:
return ExhaustiveUserTypes.Unknown;
}
}
private bool IsParentDatabaseContained(Urn parentDbUrn, CDataContainer dataContainer)
{
string parentDbName = parentDbUrn.GetNameForType("Database");
Database parentDatabase = dataContainer.Server.Databases[parentDbName];
if (parentDatabase.IsSupportedProperty("ContainmentType")
&& parentDatabase.ContainmentType == ContainmentType.Partial)
{
return true;
}
return false;
}
}
}

View File

@@ -5,11 +5,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlServer.Management.Sdk.Sfc;
using Microsoft.SqlTools.ServiceLayer.Management;
using System.Linq;
using Microsoft.SqlTools.ServiceLayer.Security.Contracts;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.Security
{
@@ -101,7 +103,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
this.isMember = new Dictionary<string, bool>();
}
public UserPrototypeData(CDataContainer context)
public UserPrototypeData(CDataContainer context, UserInfo userInfo)
{
this.isSchemaOwned = new Dictionary<string, bool>();
this.isMember = new Dictionary<string, bool>();
@@ -110,10 +112,17 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
{
this.LoadUserData(context);
}
else
{
this.name = userInfo.UserName;
this.mappedLoginName = userInfo.LoginName;
this.defaultSchemaName = userInfo.DefaultSchema;
this.password = DatabaseUtils.GetReadOnlySecureString(userInfo.Password);
}
this.LoadRoleMembership(context);
this.LoadSchemaData(context);
this.LoadSchemaData(context);
}
public UserPrototypeData Clone()
@@ -465,7 +474,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
var comparer = this.parent.GetStringComparer();
if (comparer.Compare(dbRole.Name, "public") != 0)
{
this.roleNames.Add(dbRole.Name);
roleNames.Add(dbRole.Name);
}
}
return roleNames;
@@ -483,7 +492,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
foreach (Schema sch in this.parent.Schemas)
{
this.schemaNames.Add(sch.Name);
schemaNames.Add(sch.Name);
}
return schemaNames;
}
@@ -539,7 +548,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
{
enumerator.Reset();
String? nullString = null;
string? nullString = null;
while (enumerator.MoveNext())
{
@@ -597,15 +606,13 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
}
if ((this.currentState.userType == UserType.Certificate)
&&(!this.Exists || (user.Certificate != this.currentState.certificateName))
)
&&(!this.Exists || (user.Certificate != this.currentState.certificateName)))
{
user.Certificate = this.currentState.certificateName;
}
if ((this.currentState.userType == UserType.AsymmetricKey)
&& (!this.Exists || (user.AsymmetricKey != this.currentState.asymmetricKeyName))
)
&& (!this.Exists || (user.AsymmetricKey != this.currentState.asymmetricKeyName)))
{
user.AsymmetricKey = this.currentState.asymmetricKeyName;
}
@@ -621,7 +628,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
result = this.parent.Users[this.originalState.name];
result?.Refresh();
System.Diagnostics.Debug.Assert(0 == String.Compare(this.originalState.name, this.currentState.name, StringComparison.Ordinal), "name of existing user has changed");
System.Diagnostics.Debug.Assert(0 == string.Compare(this.originalState.name, this.currentState.name, StringComparison.Ordinal), "name of existing user has changed");
if (result == null)
{
throw new Exception();
@@ -756,7 +763,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
{
//Default Schema was not supported before Denali for windows group.
User user = this.GetUser();
if (this.Exists && user.LoginType == LoginType.WindowsGroup)
if (this.Exists && user.LoginType == Microsoft.SqlServer.Management.Smo.LoginType.WindowsGroup)
{
return SqlMgmtUtils.IsSql11OrLater(this.context.Server.ConnectionContext.ServerVersion);
}
@@ -993,15 +1000,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
}
}
private UserPrototypeFactory(CDataContainer context)
private UserPrototypeFactory(CDataContainer context, UserInfo user)
{
this.context = context;
this.originalData = new UserPrototypeData(this.context);
this.originalData = new UserPrototypeData(this.context, user);
this.currentData = this.originalData.Clone();
}
public static UserPrototypeFactory GetInstance(CDataContainer context)
public static UserPrototypeFactory GetInstance(CDataContainer context, UserInfo user)
{
if (singletonInstance != null
&& singletonInstance.context != context)
@@ -1009,7 +1016,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
singletonInstance = null;
}
singletonInstance ??= new UserPrototypeFactory(context);
singletonInstance ??= new UserPrototypeFactory(context, user);
return singletonInstance;
}
@@ -1076,87 +1083,4 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
CertificateMappedUser,
AsymmetricKeyMappedUser
};
internal class LanguageUtils
{
/// <summary>
/// Gets alias for a language name.
/// </summary>
/// <param name="connectedServer"></param>
/// <param name="languageName"></param>
/// <returns>Returns string.Empty in case it doesn't find a matching languageName on the server</returns>
public static string GetLanguageAliasFromName(Server connectedServer,
string languageName)
{
string languageAlias = string.Empty;
SetLanguageDefaultInitFieldsForDefaultLanguages(connectedServer);
foreach (Language lang in connectedServer.Languages)
{
if (lang.Name == languageName)
{
languageAlias = lang.Alias;
break;
}
}
return languageAlias;
}
/// <summary>
/// Gets name for a language alias.
/// </summary>
/// <param name="connectedServer"></param>
/// <param name="languageAlias"></param>
/// <returns>Returns string.Empty in case it doesn't find a matching languageAlias on the server</returns>
public static string GetLanguageNameFromAlias(Server connectedServer,
string languageAlias)
{
string languageName = string.Empty;
SetLanguageDefaultInitFieldsForDefaultLanguages(connectedServer);
foreach (Language lang in connectedServer.Languages)
{
if (lang.Alias == languageAlias)
{
languageName = lang.Name;
break;
}
}
return languageName;
}
/// <summary>
/// Sets exhaustive fields required for displaying and working with default languages in server,
/// database and user dialogs as default init fields so that queries are not sent again and again.
/// </summary>
/// <param name="connectedServer">server on which languages will be enumerated</param>
public static void SetLanguageDefaultInitFieldsForDefaultLanguages(Server connectedServer)
{
string[] fieldsNeeded = new string[] { "Alias", "Name", "LocaleID", "LangID" };
connectedServer.SetDefaultInitFields(typeof(Language), fieldsNeeded);
}
}
internal class ObjectNoLongerExistsException : Exception
{
private static string ExceptionMessage
{
get
{
return "Object no longer exists";
}
}
public ObjectNoLongerExistsException()
: base(ExceptionMessage)
{
//
// TODO: Add constructor logic here
//
}
}
}