// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // #nullable disable using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Dmf; using Microsoft.SqlServer.Management.Sdk.Sfc; using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlTools.ServiceLayer.Management; using Microsoft.SqlTools.SqlCore.Utility; using System; using System.Collections.Generic; using System.Data; using System.IO; using System.Runtime.InteropServices; using System.Security; namespace Microsoft.SqlTools.ServiceLayer.Utility { public static class DatabaseUtils { /// /// Check if the database is a system database /// /// the name of database /// return true if the database is a system database public static bool IsSystemDatabaseConnection(string databaseName) { return (string.IsNullOrWhiteSpace(databaseName) || string.Compare(databaseName, CommonConstants.MasterDatabaseName, StringComparison.OrdinalIgnoreCase) == 0 || string.Compare(databaseName, CommonConstants.MsdbDatabaseName, StringComparison.OrdinalIgnoreCase) == 0 || string.Compare(databaseName, CommonConstants.ModelDatabaseName, StringComparison.OrdinalIgnoreCase) == 0 || string.Compare(databaseName, CommonConstants.TempDbDatabaseName, StringComparison.OrdinalIgnoreCase) == 0); } public static string AddStringParameterForInsert(string paramValue) { string value = string.IsNullOrWhiteSpace(paramValue) ? paramValue : StringUtils.EscapeStringSQuote(paramValue); return $"'{value}'"; } public static string AddStringParameterForUpdate(string columnName, string paramValue) { string value = string.IsNullOrWhiteSpace(paramValue) ? paramValue : StringUtils.EscapeStringSQuote(paramValue); return $"{columnName} = N'{value}'"; } public static string AddByteArrayParameterForUpdate(string columnName, string paramName, string fileName, Dictionary parameters) { byte[] contentBytes; using (var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read)) { using (var reader = new BinaryReader(stream)) { contentBytes = reader.ReadBytes((int)stream.Length); } } parameters.Add($"{paramName}", contentBytes); return $"{columnName} = @{paramName}"; } public static string AddByteArrayParameterForInsert(string paramName, string fileName, Dictionary parameters) { byte[] contentBytes; using (var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read)) { using (var reader = new BinaryReader(stream)) { contentBytes = reader.ReadBytes((int)stream.Length); } } parameters.Add($"{paramName}", contentBytes); return $"@{paramName}"; } public static SecureString GetReadOnlySecureString(string secret) { SecureString ss = new SecureString(); foreach (char c in secret.ToCharArray()) { ss.AppendChar(c); } ss.MakeReadOnly(); return ss; } public static bool IsSecureStringsEqual(SecureString ss1, SecureString ss2) { IntPtr bstr1 = IntPtr.Zero; IntPtr bstr2 = IntPtr.Zero; try { bstr1 = Marshal.SecureStringToBSTR(ss1); bstr2 = Marshal.SecureStringToBSTR(ss2); int length1 = Marshal.ReadInt32(bstr1, -4); int length2 = Marshal.ReadInt32(bstr2, -4); if (length1 != length2) { return false; } for (int x = 0; x < length1; ++x) { byte b1 = Marshal.ReadByte(bstr1, x); byte b2 = Marshal.ReadByte(bstr2, x); if (b1 != b2) { return false; } } return true; } finally { if (bstr2 != IntPtr.Zero) { Marshal.ZeroFreeBSTR(bstr2); } if (bstr1 != IntPtr.Zero) { Marshal.ZeroFreeBSTR(bstr1); } } } /// /// this is the main method that is called by DropAllObjects for every object /// in the grid /// /// public static void DoDropObject(CDataContainer dataContainer) { // if a server isn't connected then there is nothing to do if (dataContainer.Server == null) { return; } var executionMode = dataContainer.Server.ConnectionContext.SqlExecutionModes; var subjectExecutionMode = executionMode; //For Azure the ExecutionManager is different depending on which ExecutionManager //used - one at the Server level and one at the Database level. So to ensure we //don't use the wrong execution mode we need to set the mode for both (for on-prem //this will essentially be a no-op) SqlSmoObject sqlDialogSubject = null; try { sqlDialogSubject = dataContainer.SqlDialogSubject; } catch (System.Exception) { //We may not have a valid dialog subject here (such as if the object hasn't been created yet) //so in that case we'll just ignore it as that's a normal scenario. } if (sqlDialogSubject != null) { subjectExecutionMode = sqlDialogSubject.ExecutionManager.ConnectionContext.SqlExecutionModes; } Urn objUrn = sqlDialogSubject?.Urn; System.Diagnostics.Debug.Assert(objUrn != null); SfcObjectQuery objectQuery = new SfcObjectQuery(dataContainer.Server); IDroppable droppableObj = null; string[] fields = null; foreach (object obj in objectQuery.ExecuteIterator(new SfcQueryExpression(objUrn.ToString()), fields, null)) { System.Diagnostics.Debug.Assert(droppableObj == null, "there is only one object"); droppableObj = obj as IDroppable; } // For Azure databases, the SfcObjectQuery executions above may have overwritten our desired execution mode, so restore it dataContainer.Server.ConnectionContext.SqlExecutionModes = executionMode; if (sqlDialogSubject != null) { sqlDialogSubject.ExecutionManager.ConnectionContext.SqlExecutionModes = subjectExecutionMode; } if (droppableObj == null) { string objectName = objUrn.GetAttribute("Name"); objectName ??= string.Empty; throw new Microsoft.SqlServer.Management.Smo.MissingObjectException("DropObjectsSR.ObjectDoesNotExist(objUrn.Type, objectName)"); } //special case database drop - see if we need to delete backup and restore history SpecialPreDropActionsForObject(dataContainer, droppableObj, deleteBackupRestoreOrDisableAuditSpecOrDisableAudit: false, dropOpenConnections: false); droppableObj.Drop(); //special case Resource Governor reconfigure - for pool, external pool, group Drop(), we need to issue SpecialPostDropActionsForObject(dataContainer, droppableObj); } private static void SpecialPreDropActionsForObject(CDataContainer dataContainer, IDroppable droppableObj, bool deleteBackupRestoreOrDisableAuditSpecOrDisableAudit, bool dropOpenConnections) { // if a server isn't connected then there is nothing to do if (dataContainer.Server == null) { return; } Database db = droppableObj as Database; if (deleteBackupRestoreOrDisableAuditSpecOrDisableAudit) { if (db != null) { dataContainer.Server.DeleteBackupHistory(db.Name); } else { // else droppable object should be a server or database audit specification ServerAuditSpecification sas = droppableObj as ServerAuditSpecification; if (sas != null) { sas.Disable(); } else { DatabaseAuditSpecification das = droppableObj as DatabaseAuditSpecification; if (das != null) { das.Disable(); } else { Audit aud = droppableObj as Audit; if (aud != null) { aud.Disable(); } } } } } // special case database drop - drop existing connections to the database other than this one if (dropOpenConnections) { if (db?.ActiveConnections > 0) { // force the database to be single user db.DatabaseOptions.UserAccess = DatabaseUserAccess.Single; db.Alter(TerminationClause.RollbackTransactionsImmediately); } } } private static void SpecialPostDropActionsForObject(CDataContainer dataContainer, IDroppable droppableObj) { // if a server isn't connected then there is nothing to do if (dataContainer.Server == null) { return; } if (droppableObj is Policy) { Policy policyToDrop = (Policy)droppableObj; if (!string.IsNullOrEmpty(policyToDrop.ObjectSet)) { ObjectSet objectSet = policyToDrop.Parent.ObjectSets[policyToDrop.ObjectSet]; objectSet.Drop(); } } ResourcePool rp = droppableObj as ResourcePool; ExternalResourcePool erp = droppableObj as ExternalResourcePool; WorkloadGroup wg = droppableObj as WorkloadGroup; if (null != rp || null != erp || null != wg) { // Alter() Resource Governor to reconfigure dataContainer.Server.ResourceGovernor.Alter(); } } public static string[] LoadSqlLogins(ServerConnection serverConnection) { return LoadItems(serverConnection, "Server/Login"); } public static string[] LoadItems(ServerConnection serverConnection, string urn) { try { List items = new List(); Request req = new Request(); req.Urn = urn; req.ResultType = ResultType.IDataReader; req.Fields = new string[] { "Name" }; Enumerator en = new Enumerator(); using (IDataReader reader = en.Process(serverConnection, req).Data as IDataReader) { if (reader != null) { string name; while (reader.Read()) { // Get the permission name name = reader.GetString(0); items.Add(name); } } } items.Sort(); return items.ToArray(); } catch (Microsoft.SqlServer.Management.Sdk.Sfc.EnumeratorException) { // reading Logins can fail when trying to create a contained/SQL DB user // when the current session does not have permissions to master // we can return an empty existing login list in this scenario // no need to log here since this is an expected non-blocking exception that is recoverable return new string[0]; } } /// /// Removes invalid characters from a filename string, replacing each invalid character with an underscore. /// public static string SanitizeDatabaseFileName(string fileName) { char[] nameChars = fileName.ToCharArray(); for (int i = 0; i < nameChars.Length; i++) { if (illegalFilenameCharacters.Contains(nameChars[i])) { nameChars[i] = '_'; } } return new string(nameChars); } private static readonly HashSet illegalFilenameCharacters = new HashSet(new char[] { '\\', '/', ':', '*', '?', '"', '<', '>', '|' }); } }