{"id":738,"date":"2009-11-26T00:00:00","date_gmt":"2009-11-26T00:00:00","guid":{"rendered":"https:\/\/test.simple-talk.com\/uncategorized\/exceptional-powershell-dba-pt1-orphaned-users\/"},"modified":"2021-08-24T13:40:34","modified_gmt":"2021-08-24T13:40:34","slug":"exceptional-powershell-dba-pt1-orphaned-users","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/database-administration-sql-server\/exceptional-powershell-dba-pt1-orphaned-users\/","title":{"rendered":"Exceptional PowerShell DBA Pt1 &#8211; Orphaned Users"},"content":{"rendered":"<div id=\"PRETTY\">\n<p>Brad McGehee&#8217;s &#8220;How to Become an Exceptional DBA&#8221; is one of the more well-thumbed books in my library, right next to Kalen Delaney&#8217;s &#8220;Inside SQL Server 2000&#8221;. Brad&#8217;s book is an excellent example of recommended reading for changing the way you think <em>and<\/em> work. I admit that, the first time I read it, I thought many of Brad&#8217;s suggestions were, quite frankly, utopian. But it turns out that the &#8216;utopia&#8217; is actually a realizable goal.<\/p>\n<p>Like many of us, I have been guilty of not seeing beyond the little world of my immediate professional actions, and of being confident that nothing beyond those actions was needed for me to do my job well. With hindsight, I will say that every word Brad writes his sensational handbook is perfectly applicable to any organization, be it small or large. What makes a difference is the scale of your <em>thinking<\/em>: Big or Small.<\/p>\n<p>Brad&#8217;s lessons influenced me so much that, when I learned what little I know of PowerShell, I decided to turn his DBA checklists into PowerShell scripts, and I&#8217;d like to share some of these scripts (which I use daily) with you. To make sure this is not tiresome reading, I&#8217;ve divided my scripts into 3 parts, and the first is: <strong><em>Check\/Fix the Orphan Users<\/em><\/strong>.<\/p>\n<p>I actually decided to start with this example because it was an almost daily procedure for me (due to the circumstances at the time), and it took a considerable amount of time. As we are lovers of PowerShell, I also want to share with you how this fantastic shell solved my problem with a uniform, repeatable and consistent solution. We&#8217;ll cover more <em>usual<\/em> situations in the future articles in this little series<\/p>\n<h2>A Little Background<\/h2>\n<p>John IDontKnow, Robert IDontknowEither and You work as DBAs for a major data center in 3 daily shifts, of which yours is from 7:00 AM to 16:00 PM. All three of you have sufficient autonomy to do what you need, and you are responsible for the center&#8217;s top client, who has a complex evolving OLTP and OLAP infrastructure, and a ton of SQL Servers.<\/p>\n<p>You have a large security issue that affects your work: For contractual reasons, some of your client&#8217;s employees have access to production servers, and for their own internal reasons they drop logins almost daily. And just for fun, some logins are recreated again at later dates, but not with the same SID, making them Orphan Users. As a result, one of your main routines in the morning (and it takes a reasonable amount of time) is to check for these orphaned users.<\/p>\n<p>Your manager, a senior DBA in the company (*gasp* a tech-savvy manager) asks you to search for all the orphaned users in the environment in order to implement an appropriate security policy and send it to the client. &#8220;<em>At last<\/em>!&#8221; you happily respond, &#8220;<em>OK, I&#8217;ll do it today<\/em>&#8220;. Surely it would have been better to have answered: <em> &#8220;If you want to see that data now, I have a process that runs every night looking for orphaned users, logging the data in a table&#8221;<\/em> After all, you are an Exceptional DBA and you use PowerShell.<\/p>\n<h1>Check\/Fix the Orphan Users<\/h1>\n<p>In this script, we will work with PowerShell 2.0 and error handling (in case the SQL Servers are offline or some other problem occurs), and we will also be using a function module (.psm1) to contain the functions that we&#8217;ll use in our scripts and make them easily available; It contains:<\/p>\n<p><strong>Save-MSSQLMsg<\/strong> : This function generates a log file for scripts (Success, error , or simply an information message)<\/p>\n<p><strong>Map-MSSQLUser<\/strong> This function maps the user with their Login &#8211; It only maps Users and Logins with the same name.<\/p>\n<p><strong>Drop-MSSQLUser<\/strong> This function drops any users who don&#8217;t have a Login (it checks for Logins and Users with the same name, again)<\/p>\n<p><strong>Get-MSSQLUser<\/strong> This function Returns all an object with information about users : <code><br \/>\n        LineNumber, Date, ServerName, DatabaseName, UserName, CreateDate,<br \/>\n        DateLastModified, AsymMetricKey, DefaultSchema, HasDBAccess,ID, LoginType, Login,<br \/>\n        Orphan, LoginExists<br \/>\n        <\/code> The Orphan and LoginExists properties let you know whether a User is orphaned (i.e. does not have a Login) and whether there is Login exists with the same name as, but is not mapped to, this user.<\/p>\n<p>The first thing to do is create the two folders for the server list file and the ps1 script files (and function.psm1 module) respectively. In this case:<\/p>\n<ul>\n<li>The servers list file is stored in <em>C:\\PS\\servers\\ <\/em><\/li>\n<li>The log file is created in <em>C:\\TEMP<\/em>, which is the default path from the <strong>Save-MSSQLMsg<\/strong> function. As this path is passed as a parameter, you can very simply change this to put in a new path where the function is called<\/li>\n<li>And ps1 scripts (and <em>function.psm1 module<\/em>) are in <em>C:\\PS\\PSScripts<\/em><\/li>\n<\/ul>\n<p>The flat file containing the list of servers will have to contain <em>all<\/em> SQL Servers in your environment, and if you have more than one instance that you want to monitor, each instance must also be listed. It should looks like this:<\/p>\n<p><code><em>Server1<\/em><br \/>\n        <em>Server2<\/em><br \/>\n        <em>Server3\\Instance1<\/em><br \/>\n        <em>Server3\\Instance2<\/em><\/code><\/p>\n<p>If you do not have SQL Server 2008 but still want to use the 2008 PowerShell Provider, then <a href=\"http:\/\/vsteamsystemcentral.com\/cs21\/blogs\/timbenninghoff\/archive\/2008\/12\/21\/Installing-SQL-Server-2008-PowerShell-snap_2D00_ins-for-SQL-Server-2005.aspx\">Tim Benninghoff has an excellent guide on how to make that possible<\/a>. However, you will have take &#8216;Profiles&#8217; into account if that is the case.<\/p>\n<h3>PowerShell Profiles<\/h3>\n<p><em>When you add aliases, functions, and variables, you are actually adding them only to the current Windows PowerShell session. If you exit the session or close Windows PowerShell, the changes are lost. To retain these changes, you can create a Windows PowerShell profile and add the aliases, functions, and variables to the profiles. The profile is loaded every time that Windows PowerShell starts.&#8221;<\/em> <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/bb613488(VS.85,loband).aspx\">Windows PowerShell Profiles<\/a>.<\/p>\n<p>So how are we going to use a CmdExec job with the invoke-cmd cmdlet when the SQL Server 2008 Provider isn&#8217;t installed? Simply adjust all user profiles to load the appropriate Snapin and provider from SQL Server 2008, and avoid the problem. We also load our function module (.psm1) into the profile So that all the required functions will be available when we start the Shell.To do that, open this file (or create it if it doesn&#8217;t already exist): <strong><em>%windir%\\system32\\WindowsPowerShell\\v1.0\\Microsoft.PowerShell_profile.ps1<\/em><\/strong> &#8230;and type:<\/p>\n<pre class=\"theme:powershell-ise lang:ps\">if (!(Get-PSSnapin -registered | ?{$_.name -eq 'SqlServerProviderSnapin100'}))\r\n{\r\n    add-pssnapin SqlServerProviderSnapin100\r\n    add-pssnapin SqlServerCmdletSnapin100\r\n}\r\n\r\nImport-Module -Name'C:\\PS\\PSScripts\\Functions.psm1' -WarningAction SilentlyContinue  -ErrorAction Stop<\/pre>\n<p>If you want to see the snap-ins registered, start a new PowerShell session, type:<\/p>\n<pre class=\"theme:powershell-ise lang:ps\">Get-PSSnapin - Registered<\/pre>\n<p>&#8230; And the SQL Server 2008 should be there, as shown below :<\/p>\n<p class=\"ILLUSTRATION\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/874-img1.gif\" alt=\"874-img1.gif\" width=\"630\" height=\"240\" \/><\/p>\n<p>We can see our functions module in the same way; just type:<\/p>\n<pre class=\"theme:powershell-ise lang:ps\">Get-Module<\/pre>\n<p>And you should see this:<\/p>\n<p class=\"ILLUSTRATION\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/874-img2.gif\" alt=\"874-img2.gif\" width=\"630\" height=\"115\" \/><\/p>\n<p>Now all shells and users will be able to use the SQL Server snap-in and our functions.<\/p>\n<h2>The Scripts<\/h2>\n<p>With that done, lets actually create that function script module (functions.psm1) that will do the work of generating logs and mapping or dropping logins.<\/p>\n<pre class=\"theme:powershell-ise lang:ps\">Function Save-MSSQLMsg () \r\n&lt;#\r\n----------------------------------------------------------\r\nSave  log in file\r\n----------------------------------------------------------\r\nRequires POWERSHELL 2.0\r\n\r\nFile Name               = $NamePS1\r\nServer name             = $Server \r\nDatabaseName            = $databasename\r\nMessage To Log          = $Message\r\nPath to generate file   = $PathFileLog Default = C:\\temp\r\nDate to Log             = $TodayDate\r\n#&gt;\r\n\r\n{\r\n\r\n[CmdletBinding()]\r\n\r\nParam (\r\n[Parameter(position=1,Mandatory = $true )][String] $NamePS1,\r\n[Parameter(position=2,Mandatory = $true )][String] $Server,\r\n[Parameter(position=3,Mandatory = $false )][String] $DatabaseName = \"\",\r\n[Parameter(position=4,Mandatory = $false )][String] $Message = \"\" ,\r\n[Parameter(position=5,Mandatory = $false )][String] $PathFileLog = \"C:\\temp\",\r\n[Parameter(position=6,Mandatory = $false )][String] $TodayDate = (Get-Date -Format \r\n                                                                  \"yyyyMMddhhmmss\")\r\n)\r\nprocess \r\n{\r\n\r\n#test if path wich will contains the error file exists. if not create \r\n\r\nif (!(Test-Path -path $PathFileLog))\r\n{\r\ntry {\r\n    New-Item $PathFileLog -itemtype directory -ErrorAction  Stop   | Out-Null\r\n}\r\ncatch {\r\n    Write-Host \"Can not create log file path\"\r\n    break;\r\n}\r\n} \r\n\r\n\r\n$NameFileFull = $PathFileLog + \"\\\" + $NamePS1 + $TodayDate + \".log\" \r\n\r\n$TDate = $TodayDate.Substring(0,4) + \"-\" + $TodayDate.Substring(4,2) + \"-\" \r\n         + $TodayDate.Substring(6,2) \r\n\r\n\"Server : \" + $Server + \" Database : \" + $DatabaseName + \" Date : \"  + $TDate + \r\n          \" Message: \"  + $Message | Out-file  $NameFileFull -append \r\n} \r\n}\r\n\r\n\r\nFunction Map-MSSQLUser()\r\n{\r\n&lt;#\r\n----------------------------------------------------------\r\nMap USer and Logins\r\n----------------------------------------------------------\r\nRequires POWERSHELL 2.0\r\nServer Name         = $Server\r\nDatabase Name       = $Database\r\nUSer Name           = $USer\r\n\r\n#&gt;\r\n\r\n[CmdletBinding()]\r\n\r\nParam ( \r\n    [Parameter(Mandatory = $true )][string] $Server,\r\n    [Parameter(Mandatory = $true )][String] $Database,\r\n    [Parameter(Mandatory = $true )][String] $USer\r\n    )\r\n    \r\nprocess\r\n{\r\n\r\n$SqlConn = New-Object System.Data.SqlClient.SqlConnection\r\n$SqlCmd = New-Object System.Data.SqlClient.SqlCommand\r\n\r\ntry{\r\n    $SqlConn.ConnectionString = \"Server=\" + $Server+ \";Database=\" + $Database + \r\n                                \";Integrated Security=True\"\r\n    $sqlconn.Open()\r\n    $SqlCmd.Connection = $SqlConn\r\n    $SqlCmd.CommandText = \"ALTER USER \" + $USer + \" WITH LOGIN = \" + $USer\r\n    $SqlCmd.ExecuteNonQuery()\r\n} catch {\r\n            $msg = $error[0]\r\n            Write-Warning $msg\r\n            Save-MSSQLMsg \"Map-MSSQLUser\" \"$svr\" \"$Database\" \"$msg\" \r\n}           \r\nfinally {\r\n    $SqlConn.Close()\r\n}\r\n}\r\n\r\n}\r\n\r\n\r\nFunction Drop-MSSQLUser ()\r\n{\r\n&lt;#\r\n----------------------------------------------------------\r\nDrop USer without Login\r\n----------------------------------------------------------\r\nRequires POWERSHELL 2.0\r\nServer Name         = $Server\r\nDatabase Name       = $Database\r\nUSer Name           = $USer\r\n\r\n#&gt;\r\n\r\n[CmdletBinding()]\r\n\r\nParam ( \r\n    [Parameter(Mandatory = $true )][string] $Server,\r\n    [Parameter(Mandatory = $true )][String] $Database,\r\n    [Parameter(Mandatory = $true )][String] $USer\r\n    )\r\n    \r\nprocess\r\n{\r\n\r\n$SqlConn = New-Object System.Data.SqlClient.SqlConnection\r\n$SqlCmd = New-Object System.Data.SqlClient.SqlCommand\r\n\r\ntry{\r\n    $SqlConn.ConnectionString = \"Server=\" + $Server+ \";Database=\" + $Database + \r\n                                \";Integrated Security=True\"\r\n    $sqlconn.Open()\r\n    $SqlCmd.Connection = $SqlConn\r\n    $SqlCmd.CommandText = \"Drop  USER \" + $USer \r\n    $SqlCmd.ExecuteNonQuery()\r\n} catch {\r\n            $msg = $error[0]\r\n            Write-Warning $msg\r\n            Save-MSSQLMsg \"Drop-MSSQLUser\" \"$svr\" \"$Database\" \"$msg\" \r\n}           \r\nfinally {\r\n    $SqlConn.Close()\r\n}\r\n}\r\n\r\n}\r\n\r\n\r\nFunction Get-MSSQLUser() \r\n\r\n{\r\n&lt;#\r\nTXTServersList = File List (.txt) with  the servers. Default is the current server\r\n\r\n#&gt;\r\n\r\n\r\n[CmdletBinding()]\r\n\r\nPARAM(\r\n\r\n    [Parameter(position=1,Mandatory=$false, ValueFromPipelineByPropertyName=$true,\r\n               HelpMessage=\"File Servers List\")]\r\n    [Alias(\"FullNameTXT\")]\r\n    [String] $TXTServersList = $env:COMPUTERNAME\r\n\r\n)   \r\n\r\n\r\nbegin\r\n{\r\n    \r\n$verbosePreference=\"continue\" \r\n[reflection.assembly]::LoadWithPartialName(\"Microsoft.SqlServer.Smo\") | out-null \r\n}\r\nprocess \r\n{\r\n\r\ntry \r\n{\r\n\r\n    if ($TXTServersList.substring($TXTServersList.length -4,4) -eq \".TXT\")\r\n    {\r\n        try\r\n        {\r\n            $ServersList = get-content $TXTServersList  \r\n        } catch {\r\n                    $msg = $error[0]\r\n                    Write-Warning $msg\r\n                    break;\r\n        }\r\n    }   \r\n    else\r\n    {\r\n        $ServersList = $TXTServersList\r\n    }   \r\n    \r\n    \r\n    $LineNumber = 1\r\n    $FinalResult = @()\r\n\r\n    foreach ($svr in  $ServersList )\r\n    {\r\n        try \r\n        {\r\n            $Server=New-Object \"Microsoft.SqlServer.Management.Smo.Server\" \"$svr\"\r\n            $Server.Databases  | where-object {$_.IsSystemObject -eq $FALSE -and \r\n                                               $_.IsAccessible -eq $TRUE } | foreach {\r\n        \r\n                $Database = $_.name\r\n                foreach  ($user in $Server.Databases[$_.name].users ) {\r\n                \r\n                    $Object = New-Object PSObject\r\n            \r\n                    [datetime] $CreateDate = \"{0:yyyy\/MM\/dd hh:mm:ss}\" -f [datetime] \r\n                                             $user.CreateDate  \r\n                    if ($user.DateLastModified -ne $null)\r\n                    {\r\n                        [datetime] $DateLastModified = \"{0:yyyy\/MM\/dd hh:mm:ss}\" -f \r\n                                                    [datetime] $user.DateLastModified \r\n                    }                               \r\n                \r\n               $Object | add-member Noteproperty LineNumber       $LineNumber     \r\n               $Object | add-member Noteproperty Date             $TodayDate  \r\n               $Object | add-member Noteproperty ServerName       $svr\r\n               $Object | add-member Noteproperty DatabaseName     $Database\r\n               $Object | add-member Noteproperty UserName         $user.name\r\n               $Object | add-member Noteproperty CreateDate       $CreateDate\r\n               $Object | add-member Noteproperty DateLastModified $DateLastModified\r\n               $Object | add-member Noteproperty AsymMetricKey    $user.AsymMetricKey\r\n               $Object | add-member Noteproperty DefaultSchema    $user.DefaultSchema\r\n               $Object | add-member Noteproperty HasDBAccess      $user.HasDBAccess\r\n               $Object | add-member Noteproperty ID               $user.ID\r\n               $Object | add-member Noteproperty LoginType        $user.LoginType\r\n               $Object | add-member Noteproperty Login            $user.Login\r\n               $Object | add-member Noteproperty Orphan           ($user.Login -eq \"\")\r\n                    \r\n                    $ExistLogin = $Server.logins | where-object {$_.isdisabled -eq \r\n                                  $False -and $_.IsSystemObject -eq $False -and \r\n                                  $_.IsLocked -eq $False -and $_.name -eq $user.name }  \r\n                    \r\n                    if (!$ExistLogin)\r\n            \r\n                    {   $Object | add-member Noteproperty LoginExists       $False  }\r\n                    else\r\n                    {   $Object | add-member Noteproperty LoginExists       $True   }   \r\n                    \r\n                    \r\n                    $FinalResult += $Object\r\n                    $LineNumber ++ \r\n                } \r\n                \r\n            }   \r\n            \r\n            \r\n        } catch {\r\n                $msg = $error[0]\r\n                Write-Warning $msg\r\n                Save-MSSQLMsg \"Get-MSSQLUser\" \"$svr\" \"$Database\" \"$msg\" \r\n        } Finally { \r\n                continue    \r\n        }\r\n    }\r\n    Write-Output $FinalResult   \r\n    \r\n}catch {\r\n        $msg = $error[0]\r\n        Write-Warning $msg\r\n        Save-MSSQLMsg \"Get-MSSQLUser\" \"\" \"$Database\" \"$msg\" \r\n}\r\n}   \r\n\r\n}<\/pre>\n<p>This is saved in the Windows PowerShell Script Module format file (.psm1) under the file path for all of our scripts (<em>C:\\PS\\PSScripts\\Functions.<\/em><em>psm1<\/em>) . Some sample commands you could pass to this module include:<\/p>\n<pre class=\"theme:powershell-ise lang:ps\">#Return All Users (current server - we do not pass .txt) and all Properties\r\nGet-MSSQLUser    \r\n\r\n#Return All Users and all Properties by .txt\r\nGet-MSSQLUser C:\\PS\\servers\\servers.txt<\/pre>\n<p>Now it&#8217;s time to look for those Orphaned Users.<\/p>\n<h2>How to Return Orphaned Users<\/h2>\n<p>The <strong>Orphan<\/strong> property tells us if a given user has a associated Login. If not, it&#8217;s a orphaned user &#8211; it&#8217;s that simple. We use it in conjunction with the <strong>hasdbaccess <\/strong>property too, to list only users with access to the database in question.<\/p>\n<pre class=\"theme:powershell-ise lang:ps\">#Return All Orphaned Users By .txt\r\nGet-MSSQLUser C:\\PS\\servers\\servers.txt | Where-Object {$_.orphan -eq $true -and \r\n        $_.hasdbaccess -eq $true } \r\n        | format-table Date,ServerName,DatabaseName,UserName,Login,Orphan,LoginExists\r\n\r\n#Return All Orphaned Users  current server\r\nGet-MSSQLUser | Where-Object {$_.orphan -eq $true -and $_.hasdbaccess -eq $true } \r\n        | format-table Date,ServerName,DatabaseName,UserName,Login,Orphan,LoginExists<\/pre>\n<p>We can also list all users who are orphaned but share their name with a Server Login; we just need to add the <strong>LoginExists<\/strong> property to the conditions.<\/p>\n<pre class=\"theme:powershell-ise lang:ps\">#Return All Orphaned Users By .txt\r\nGet-MSSQLUser C:\\PS\\servers\\servers.txt | Where-Object {$_.orphan -eq $true -and \r\n         $_.hasdbaccess -eq $true -and $_.LoginExists -eq $true } \r\n         | format-table Date,ServerName,DatabaseName,UserName,Login,Orphan,LoginExists<\/pre>\n<p>Any users this returns just need to be mapped with their logins.<\/p>\n<h3>What about outputting this data to a SQL Server table?&lt;<\/h3>\n<p>First, create the necessary tblOrphanedUsers table in the data repository&#8230;<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">CREATE TABLE tblOrphanedUsers    ( Users XMLCol\r\n    )<\/pre>\n<p>And then run this:<\/p>\n<pre class=\"theme:powershell-ise lang:ps\">(Get-MSSQLUser  | Where-Object {$_.orphan -eq $true -and $_.LoginExists -eq $true} \r\n| select Date,ServerName,DatabaseName,UserName,Login,Orphan,LoginExists \r\n| ConvertTo-Xml -NoTypeInformation).save(\"c:\\temp\\Users.xml\") \r\n$XML = (Get-Content c:\\temp\\users.xml ) -replace \"'\", \"''\"\r\n$SQL = \"insert into tblOrphanedUsers (XMLCol) values ('$XML')\" \r\ninvoke-sqlcmd -ServerInstance $env:COMPUTERNAME -database \"tempdb\" -query $sql<\/pre>\n<p>You can choose the properties, path and name of your XML File, and obviously you server and database repositories ( <span class=\"style1\">-ServerInstance <\/span>and <span class=\"style1\">-database<\/span>). But you want to get this information in a SQL table, yes? No problem, It&#8217;s a simple Xquery, and you can see the results below:<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">SELECT    t2.Col1.value('(Property[@Name=\"Date\"]\/text())[1]', 'nvarchar(255)') [Date],\r\n            t2.Col1.value('(Property[@Name=\"ServerName\"]\/text())[1]', 'nvarchar(255)')\r\n            ServerName,\r\n            t2.Col1.value('(Property[@Name=\"DatabaseName\"]\/text())[1]',\r\n            'nvarchar(max)') DatabaseName,\r\n            t2.Col1.value('(Property[@Name=\"UserName\"]\/text())[1]', 'nvarchar(255)') \r\n            UserName,\r\n            t2.Col1.value('(Property[@Name=\"Login\"]\/text())[1]', 'nvarchar(255)')\r\n            [Login],\r\n            t2.Col1.value('(Property[@Name=\"Orphan\"]\/text())[1]', 'nvarchar(255)') \r\n            Orphan,\r\n            t2.Col1.value('(Property[@Name=\"LoginExists\"]\/text())[1]', 'nvarchar(255)'\r\n            ) LoginExists\r\n    FROM dbo.tblOrphanedUsers\r\n            CROSS APPLY XMLCol.nodes('\/Objects\/Object') AS t2(Col1)\r\n    WHERE t2.Col1.value('(Property[@Name=\"ServerName\"]\/text())[1]', 'nvarchar(max)') =             'Jupiter'<\/pre>\n<p class=\"ILLUSTRATION\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/874-img3.gif\" alt=\"874-img3.gif\" width=\"630\" height=\"324\" \/><\/p>\n<h2>How to Fix Orphaned Users with PowerShell<\/h2>\n<p>In this case, we can fix these orphaned users in two ways. One is by mapping them to their Login (which has the same name) and another is by dropping the users who don&#8217;t share their names with a Login. Let&#8217;s take a look at these processes now.<\/p>\n<h3>Mapping Users with their Login<\/h3>\n<p>Nothing could be simpler. First, filter the objects by the <strong>orphan<\/strong> and <strong>LoginExists<\/strong> properties (the latter being &#8216;true&#8217;), and then call the <strong>Map-MSSQLUser<\/strong> function:<\/p>\n<pre class=\"theme:powershell-ise lang:ps\">#Mapping USers with Logins\r\nforeach ($user in Get-MSSQLUser c:\\PS\\Servers\\servers.txt | Where-Object \r\n{$_.orphan -eq $true -and $_.LoginExists -eq $true} ) {\r\nMap-MSSQLUser $user.ServerName $user.DatabaseName $user.UserName\r\n}<\/pre>\n<h3>Dropping users without a Login<\/h3>\n<p>This is just as simple to do as the mapping; we just change the <strong>LoginExists<\/strong> filter to &#8216;false&#8217;, and the call the <strong>Drop-MSSQLUser<\/strong> function from our PowerShell module:<\/p>\n<pre class=\"theme:powershell-ise lang:ps\">#Dropping users without login\r\nforeach ($change in Get-MSSQLUser    c:\\PS\\Servers\\servers.txt | Where-Object \r\n{$_.orphan -eq $true -and $_.LoginExists -eq $false} ) {\r\nDrop-MSSQLUser $change.ServerName $change.DatabaseName $change.UserName\r\n}<\/pre>\n<p>Bear in mind that if we omit the server path in the <strong>Get-MSSQLUser<\/strong> function call, the current server is used.<\/p>\n<p>As I said earlier, for the purposes of keeping track of what&#8217;s going on, the <strong>Save-MSSQLMsg ()<\/strong> function generates a log file of errors ,successes and even just status messages in the $PathLogFile folder (in this case we use default value c:\\temp). To identify each log, it uses the name of the running script (passed as a parameter) plus the date and time, as the figure below shows:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/874-img8.gif\" alt=\"874-img8.gif\" \/><\/p>\n<p>This will make tracking down the details for any particular scheduled job incredibly easy, and as you can see, the log contents themselves are fairly well formatted:<\/p>\n<div class=\"ILLUSTRATION\">\n<p class=\"ILLUSTRATION\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/874-img2.jpg\" alt=\"874-img2.jpg\" width=\"630\" height=\"60\" \/><\/p>\n<\/div>\n<h2>Scheduling the Check.<\/h2>\n<p>When scheduling, we can simply take the examples of mapping and dropping users above, save them into .ps1 file and run it. Let&#8217;s say we save script to Map users (as seen above) into a file called <strong>Map<\/strong><strong>OrphanedUsers.ps1<\/strong> . If you are using SQL Server 2008, you would just need to then create a PowerShell Job and run the .Ps1 script by entering the storage path of your scripts into the &#8216;command&#8217; field:<\/p>\n<pre class=\"theme:powershell-ise lang:ps\">c:\\ps\\psscripts\\MapOrphanedUsers.ps1<\/pre>\n<div class=\"ILLUSTRATION\">\n<p class=\"ILLUSTRATION\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/874-img6.jpg\" alt=\"874-img6.jpg\" width=\"630\" height=\"566\" \/><\/p>\n<\/div>\n<p>Alternatively, you can run the script with a cmdexec-type job, ensuring the command field contains:<\/p>\n<pre class=\"theme:powershell-ise lang:ps\">powershell -noprofile -Noninteractive -command c:\\ps\\psscripts\\OrphanedUsers.ps1 <\/pre>\n<p>After that, you just need to schedule the job, and you&#8217;re finished! Clearly, this is a very particular case where others users can drop logins behind your back, but let&#8217;s go to a more normal scenario:<\/p>\n<h2>Mapping Users after Restore<\/h2>\n<p>In this Datacenter, You have a Job that runs every Sunday and restores the production Databases to a development environment. So in this case, your production server X restores N databases to the development server D1, the production server Y restore N databases for development D2&#8230; Etc.<\/p>\n<p>So , to make sure you can chose which file list the servers will be passing as parameters in your powershell script, you configure a flat <strong>ServersRestore.txt<\/strong> file especially for this scenario, and it looks like this :<\/p>\n<pre>Earth, DB1, DB2, DB3 Earth\\Instance1. DB7, DB8 Sun, DB3, DB2, DB4<\/pre>\n<p>The list format is, fairly obviously, Server Instance followed by individual databases to be restored, all separated by commas. With that done, you configure one last job to run at 6:00 AM (after all the other restore jobs have finished) to fix the inevitable orphaned users in all the servers and databases listed in <strong>ServersRestore.txt<\/strong>. In this script, we load this flat file of servers and databases to check, and then the process proceeds in a very similar way to what I&#8217;ve outlined above.<\/p>\n<p>The <strong>Orphan<\/strong> property tells us the relevant status of a user and <strong>LoginExists<\/strong> will let us decide which action needs to be taken; if <strong>LoginExists<\/strong> is false, we drop this user, and if it&#8217;s True we map them.<\/p>\n<pre class=\"lang:ps theme:powershell-ise\">foreach($ServerList in Get-Content c:\\TEMP\\ServersRestore.txt)\r\n{\r\n    [array]$ServerDBList = $ServerList.split(\",\")\r\n    $TotalServerDBList = ($ServerDBList.count) -1\r\n    \r\n    $ServerName = $serverdblist[0]\r\n    for ($Count = 1;$Count -le $TotalServerDBList;$Count++)\r\n    {\r\n        $DatabaseName =$serverdblist[$count]\r\n        \r\n        foreach ($Db in Get-MSSQLUser | Where-Object {$_.orphan -eq $true -and \r\n              $_.servername -eq $ServeraName -and $_.Databasename -eq $DatabaseName })    \r\n        {\r\n            if ($_.LoginExists -eq $false)\r\n            {\r\n                Drop-MSSQLUser $Db.ServerName $Db.DatabaseName $db.username\r\n            }\r\n            else\r\n            {\r\n                Map-MSSQLUser $Db.ServerName $Db.DatabaseName $db.username\r\n            }\r\n    \r\n        }\r\n    }\r\n}<\/pre>\n<p class=\"CAPTION\">(This script as available for download at the top of this article.)<\/p>\n<p>If you want to log all the completed transactions, you just need to add the <strong>Save-MSSQLMsg<\/strong> function after each action (drop or map), and then if some problem occurs when trying to Map or Drop, the error message is already logged and ready for inspection.<\/p>\n<p>You can easily apply these functions to all databases in your environment, too. &#8220;Why would I want to do that?&#8221;, you ask. How about a migration of servers using backup and restore? Regardless of when and where you use these scripts, they can be scheduled in exactly the same way as I demonstrated earlier.<\/p>\n<p>That&#8217;s everything for this first part of my script-sharing, and this is how I check and fix orphaned users. You can find a complete set of example scripts available for download at the top right of this article. Later on I will be showing you more daily checks, making you an Exceptional DBA, and using PowerShell.<\/p>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Inspired by Brad McGehee&#8217;s &#8216;How to Become an Exceptional DBA&#8217;, Laerte decided it was high time he took a proactive approach to his work. Using PowerShell, he came up with a solution that makes finding and fixing orphaned users as easy as cake, and now he&#8217;s sharing it with us.&hellip;<\/p>\n","protected":false},"author":221715,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143527],"tags":[4170,5024,4635,4150],"coauthors":[6819],"class_list":["post-738","post","type-post","status-publish","format-standard","hentry","category-database-administration-sql-server","tag-database-administration","tag-exceptional-dba","tag-powershell","tag-sql"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/738","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/221715"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=738"}],"version-history":[{"count":14,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/738\/revisions"}],"predecessor-version":[{"id":78581,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/738\/revisions\/78581"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=738"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=738"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=738"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=738"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}