Delete Windows profiles (DeleteProfiles2K8.vbs)

Various solutions exist "out there" for deleting Windows profiles; ranging from the Windows Group Policy setting "delete cached copies of roaming profiles" (doesn't work well enough to use) through scripted solutions, to custom utilities.

Previously Microsoft had a utility called "DelProf.exe" to clean up the local profile structure. However, DelProf no longer works on Windows 7 or Server 2008. Other simplistic solutions such as deleting local profile folders engender more problems than they solve by creating system inconsistencies that cause temporary user profiles to be created.

Most prominent among the custom utilities that have caught my attention is "DelProf2" by Helge Klein (see: http://www.sepago.de/d/helge/2008/10/16/deleting-a-local-user-profile-not-as-easy-as-one-might-assume ). Another tool worth looking at is "REMProf" downloadable at http://www.ctrl-alt-del.com.au/CAD_TSUtils.htm

While the above utilities may be fine, I am reluctant to install third party tools whose inner workings are not totally transparent. Putting aside my personal preferences, many of my customers are just as skeptical; hence the quest for a workable scripted solution.

At first Joe Shonk's (http://www.theshonkproject.com) DeleteProfiles.vbs script looked promising, however it fails on Windows Vista, 7 and Server 2008. The script also has flaws that cause errors to pass undetected, giving the appearance of running successfully while in fact failing.

I have tried to fix the script's major flaws while also implementing the necessary changes to fully support Windows Server 2008 and Windows 7. The script deletes local user profiles based on criteria such as age, whether it is a cached copy of a roaming profile, or name (supports custom inclusion and exclusion lists).

You can download DeleteProfiles2K8.vbs from HERE:  https://docs.google.com/open?id=0B7HBDOaiFbCVMmM1YjgxODYtMzNlNS00MzgyLWI0ZDYtZGQwNjA0YmVhYzkw
(the file extension ".TXT" has been added to circumvent Google docs' script filtering; remove the extension after downloading)


Why pigs will fly (or the curious case of UPHClean and the SYSTEM profile)

My first response to finding domain Group Policy user settings in the SYSTEM profile on our terminal servers was "and pigs will fly" - i.e. not bl**** likely!

Still, the fact remained: restrictive domain policies in the domain were creeping into the SYSTEM profile (HKEY_USERS\S-1-5-18 -- for which incidentally HKEY_USERS\.DEFAULT is an alias). In most cases this wouldn't be a problem, but for us it was, since SCCM machine installation packages were mysteriously failing on the affected servers, and on closer inspection it turned out that the local system account was being denied running install packages! Hmm... strange ... we're talking about localsystem here! Digging into the registry we found the key and value  HKEY_USERS\S-1-5-18\Software\Microsoft\Windows\CurrentVersion \Policies\Explorer\RestrictRun that limits perimitted applications to those listed under the RestrictRun key. The application list was easily recognizable as the same one set in the domain user Group Policy "Administrative templates/System/Run only allowed Windows applications" - and does not include msiexec.exe.

This is all fine and well when applied to users John and Jane logging onto the terminal servers, but not for the local system account! So how was this user setting creeping into the SYSTEM profile? Even when we removed the setting, it reappeared within a week or so! Be assured: all likely and not so likely explanations were considered, and I'll spare you the boring details. More or less on a hunch, I disabled UPHClean (v. on the affected servers: 

The User Profile Hive Cleanup service helps to ensure user sessions are completely terminated when a user logs off. System processes and applications occasionally maintain connections to registry keys in the user profile after a user logs off. In those cases the user session is prevented from completely ending. 
This seemed to stop the problem from reappearing - but what was going on? Completely disabling UPHClean was also not an attractive long term solution, so we had to dig deeper. The following from the readme file is a hint:
UPHClean assists the operating system to unload user profile hive by remapping the handles to the user profile hive to the default user hive. For example if a process has a handle to HKEY_USERS\S-1-5-21-X-Y-Z\Software\Microsoft after remapping it would have a handle to HKEY_USERS\.DEFAULT\Software\Microsoft.  This allows the profile hive to unload.
Hmmm... what if the process owning the remapped handle decides to write data to the user hive, and what if that data happened to be policy values? Seems awfully close to what's going on. The misbehaving servers were monitored to find out exactly when the settings turned up in the system profile. UPHClean logs application log event ID 1401 when it remaps a handle and interestingly there are in fact some hits on event 1401 mentioning various "Policies" subkeys in a time frame of maximum 60 minutes before the system profile received the value in all observed cases. 

The following test nailed it:

  1. Log on user "A" to a machine running UPHClean.
  2. Log on user "B" in a new session on the same machine.
  3. Connect to the currently loaded user hive for user "A" with regedit.
  4. Create a key and a value under "Software", for example key name "Foo", Value name "Bar" (REG_SZ) = "Test value". This is now stored in users A's hive.
  5. Double-click "Bar" and modify the contents to "New test value" (but do *not* close the "Edit string" dialog yet)  
  6. Log off user "A", and check the app event log. You should now see UPHClean event id 1401.
  7. Now close the "Edit string" dialog opened in step (5).
  8. Check under HKEY_USERS\S-1-5-18\Software: Magically, you'll now find the value HKEY_USERS\S-1-5-18\Software\Foo\Bar = "New test value" in that hive!
The fix
Fortunately UPHClean can be tweaked to change the default behaviour. In our case we decided to disable handle remapping entirely by setting the config value REMAP_HANDLE_PROCESS_LIST to a single dash ("-"). By default this setting is an asterix ("*") interpreted as "remap for all processes". The full path to the setting is:
HKLM\SYSTEM\CurrentControlSet\Services\UPHClean\Parameters REMAP_HANDLE_PROCESS_LIST
It would be nice to be able to configure this using a custom admin template and Group Policy, however, it is a REG_MULTI_SZ value, so you need to script it one way or another.

I'm told UPHClean 2.0 and the "User Profile service" in Windows Server 2008 implement handle remapping a bit differently, so hopefully this will not be a problem with those versions. But with UPHClean 1.6.30 in its default configuration you can get user Group Policy settings in the localsystem profile  - and pigs will fly.


Viewing "Standard Reports" in SQL Server 2005 Management Studio for non-SA users

The "Standard Reports" in SQL Server 2005 Management Studio are pretty neat, but I was surprised to find that other power users who were granted access to the server had the entire "Standard Reports" item missing from their context menus. It should have looked something like this:

but instead they got this:

I couldn't find anything about what permissions the user needs in the official documentation, but finally found a mention of "VIEW_SERVER_STATE" permissions relevant to this. Granting the users' logins this right seems to have solved the problem - with the caveat that there could be security implications of granting normal users a right that is normally reserved for the sysadmin:


SCCM and secure secondary site

This had us scratching our collective heads for a while.

We just updated our SMS 2003 Advanced Clients to SCCM 2007 Clients and "everything" just stopped working on a secondary site, and CCMEXEC.LOG on the client systems contained multiple messages like
[CCMHTTP] HTTP ERROR: URL=http://PRIMARY_SITE_SERVER/ccm_system_windowsauth/request, Port=80, Protocol=http, SSLOptions=0, Code=12029, Text=ERROR_WINHTTP_CANNOT_CONNECT CCMEXEC 10/13/2009 11:31:20 AM 2440 (0x0988)

The key point here is that a firewall controls traffic between the secondary and primary site, and we *thought* all necessary ports were already open. Being of a simple and trusting nature we thought our main concern would be the traffic between site systems, and that by defining a Proxy Management Point we need not concern ourselves with traffic from clients to the primary site. We were wrong. It turns out that every client system needs to talk on http (tcp port 80) to the primary site server - (or, more precisely to the SLP) - at least once to find its MP.

The first clue to this solution was this post: Clients in Secondary secured zone problem
In hindsight, all should have been clear if we had really really really RTFM: Ports Used by Configuration Manager

Anyway, port 80 is now open and all is well.


NTFS security pitfalls

I often find that administrators are diligent in setting NTFS security ACLS in folders shared out from a server, only to neglect the all too common entry "CREATOR OWNER:(OI)(CI)(IO)F".

So what's the consequence of this? Any user who creates a subfolder or file automatically gets "full control" of the object and could conceivably deny access to both administrators and the SYSTEM account, thus usurping the administrators authority and creating all kinds of trouble.

So, on any NTFS file system where I care about controlling who has access to what, one of the first things I run after installing the OS is:
cacls <DRIVE>:\ /T /E /R "CREATOR OWNER"

or other command / procedure that removes "CREATOR OWNER" from all access lists .


SQL Server stored procedure to return file information

An earlier version of SQL Server had the undocumented procedure "xp_getfileinfo" to return file information, but alas, it has been dropped from SQL Server 2005. On searching the 'net there are a few suggested workarounds.

One uses the "sp_OA*" procedures to access the FileSystemObject and return the requested information, others solve the task by implementing a CLR function. A third approach relies on "xp_cmdshell" and interpreting the output from a "dir" command. The last approach appeals the most to me, however, there's a problem if you want to be locale independent, the date and time formats produced by dir vary: dd/mm/yyyy, mm.dd.yyyy, hh:mm (24 hour format) hh:mmam/pm, etc. So the challenge is to interpret dir's output and return the information in a standard format. I decided on a two step process where the first step samples the output of a larger number of files to infer the current date format, with the second step applying the results to returning the info for the requested file. Here it is:
IF OBJECT_ID('dbo.p_FileInfo', 'P') IS NOT NULL DROP PROCEDURE dbo.p_FileInfo
    (@p_FileName varchar(250),
     @p_FileDate varchar(20) = NULL OUTPUT,  -- Format YYYYMMDD_HHMM
     @p_FileSize int = NULL OUTPUT)          -- Size in bytes

    -- Check if file exists, otherwise don't bother ...
    declare @err int
    exec master.dbo.xp_fileexist @p_FileName, @err OUTPUT;
    if @err = 0 return(1);

    -- The first part of this procedure tries to determine the current date format
    -- used in "DIR" output, by analysing a sampling of the output.
    CREATE TABLE #diroutput(line varchar(100), sDate varchar(20), sTime varchar(10), 
                            FileSize int, sName varchar(100))

    insert #diroutput(line) Exec master.dbo.xp_cmdshell 
        'dir /A-D /TW /-C /4 %SYSTEMROOT%\TEMP %TEMP% | findstr /R "^[0-9]"'
    delete #diroutput where line is null

    declare @line varchar(100);
    declare @line1 varchar(100);

    set @line = (select top 1 line from #diroutput);

    declare @iDateStart smallint
    declare @iDateEnd smallint
    declare @iTimeStart smallint
    declare @iTimeEnd smallint
    declare @iSizeStart smallint
    declare @iSizeEnd smallint
    declare @iNameStart smallint

    set @iDateEnd = CHARINDEX(' ', @line);
    set @line1 = STUFF(@line, 1, @iDateEnd, REPLICATE(' ', @iDateEnd));

    set @iTimeStart = @iDateEnd + 1;
    set @iTimeEnd = CHARINDEX(' ', @line1, PATINDEX('%[0-9]%', @line1));
    set @line1 = STUFF(@line, 1, @iTimeEnd, REPLICATE(' ', @iTimeEnd));
    if LTRIM(@line1) LIKE '[AP]M %'
        set @iTimeEnd = CHARINDEX(' ', @line1, CHARINDEX('M', @line1));
        set @line1 = STUFF(@line, 1, @iTimeEnd, REPLICATE(' ', @iTimeEnd));

    set @iSizeStart = @iTimeEnd + 1;
    set @iSizeEnd = CHARINDEX(' ', @line1, PATINDEX('%[0-9]%', @line1));
    set @line1 = STUFF(@line, 1, @iSizeEnd, REPLICATE(' ', @iSizeEnd));

    set @iNameStart = @iSizeEnd + 1;

    UPDATE #diroutput SET
           sDate = left(line, @iDateEnd-1)
          ,sTime = LTRIM(substring(line, @iTimeStart, @iTimeEnd - @iTimeStart))
          ,FileSize = convert(int, LTRIM(substring(line, @iSizeStart, 
                                                   @iSizeEnd - @iSizeStart)))
          ,sName = LTRIM(substring(line, @iNameStart, LEN(line) - @iNameStart + 1));

    declare @sDateSample varchar(20)
    declare @dateSep char(1)
    set @sDateSample = (select top 1 sDate from #diroutput);
    set @dateSep = SUBSTRING(@sDateSample, PATINDEX('%[^0-9]%', @sDateSample), 1);

    declare @sTimeSample varchar(10)
    declare @timeSep char(1)
    set @sTimeSample = (select top 1 sTime from #diroutput);
    set @timeSep = SUBSTRING(@sTimeSample, PATINDEX('%[^0-9]%', @sTimeSample), 1);

    -- Three parts of a datestring
    declare @DateParts 
        TABLE (
            PartNo smallint, 
            PartStart smallint, 
            PartLen smallint, 
            PartName varchar(10));

    declare @iPartStart smallint, @iPartLen smallint

    -- First part
    set @iPartStart = 1;
    set @iPartLen = CHARINDEX(@dateSep, @sDateSample) - 1;
    INSERT @DateParts(PartNo, PartStart, PartLen) SELECT 1, @iPartStart, @iPartLen;
    -- Second part
    set @iPartStart = @iPartStart + @iPartLen + 1;
    set @iPartLen = CHARINDEX(@dateSep, @sDateSample, @iPartStart) - @iPartStart;
    INSERT @DateParts(PartNo, PartStart, PartLen) SELECT 2, @iPartStart, @iPartLen;
    -- Third part
    set @iPartStart = @iPartStart + @iPartLen + 1;
    set @iPartLen = LEN(@sDateSample) - @iPartStart + 1;
    INSERT @DateParts(PartNo, PartStart, PartLen) SELECT 3, @iPartStart, @iPartLen;

    UPDATE dp
        set PartName = case
                          when dpMax.maxval <= 12 then 'MONTH'
                          when dpMax.maxval <= 31 then 'DAY'
                          else 'YEAR'
    FROM @DateParts dp JOIN
        (select max(PartNo) As PartNo, max(substring(sDate, PartStart, PartLen)) As maxval
         from #diroutput o cross join @DateParts p group by p.PartNo) dpMax 
              on dp.PartNo = dpMax.PartNo;

    -- In the second part we retrieve the information about the file
    declare @cmd varchar(1000)
    TRUNCATE TABLE #diroutput
    set @cmd = 'dir /A-D /TW /-C /4 "' + @p_FileName + '" | findstr /R "^[0-9]"'
    insert #diroutput(line) Exec master.dbo.xp_cmdshell @cmd
    delete #diroutput where line is null

    UPDATE #diroutput SET
           sDate = left(line, @iDateEnd-1)
          ,sTime = LTRIM(substring(line, @iTimeStart, @iTimeEnd - @iTimeStart))
          ,FileSize = convert(int, LTRIM(substring(line, @iSizeStart, 
                                                   @iSizeEnd - @iSizeStart)))
          ,sName = LTRIM(substring(line, @iNameStart, LEN(line) - @iNameStart + 1));

    -- Perform AM/PM conversion to 24 hour clock (just in case)
    UPDATE #diroutput SET
        sTime = case
                    when sTime LIKE '12%a%'     then 
                        '00' + SUBSTRING(sTime, 4, 2)
                    when sTime LIKE '0%p%'      then
                        convert(varchar(2), 12 + convert(int, left(sTime, 2))) 
                        + SUBSTRING(sTime, 4, 2)
                    when sTime LIKE '1[01]%p%'  then
                        convert(varchar(2), 12 + convert(int, left(sTime, 2))) 
                        + SUBSTRING(sTime, 4, 2)
                    else left(sTime, 2) + SUBSTRING(sTime, 4, 2)

    SELECT @p_FileDate = SUBSTRING(sDate, Y.PartStart, Y.PartLen) 
                       + SUBSTRING(sDate, M.PartStart, M.PartLen) 
                       + SUBSTRING(sDate, D.PartStart, D.PartLen) 
                       + '_' + sTime,
          @p_FileSize = FileSize
        FROM #diroutput
        CROSS JOIN (select PartStart, PartLen from @DateParts where PartName = 'YEAR') as Y
        CROSS JOIN (select PartStart, PartLen from @DateParts where PartName = 'MONTH') as M
        CROSS JOIN (select PartStart, PartLen from @DateParts where PartName = 'DAY') as D;



Recovering Exchange 2003 clustered on Windows 2003 Server

Arrived at work to find that my dev Exchange cluster was down - the reason being that somebody had deleted the cluster name AND the virtual Exchange server name from AD. Everything else intact, so what to do? Re-install the cluster service? Wipe everything? It turns out the solution was simple :-)

Step 1 - repair the admin group (containing the cluster name resource)
Created a new computer account in AD and made sure that the cluster service account had the appropriate access on the object (used settings from another operational cluster as template for this). Tried to bring the admin group online .... bingo!
Step 2 - repair the Exchange virtual server group
This was a bit more complex. First the foobar name resource has to be deleted, but Cluster Admin also wants to delete the other resources that have direct and indirect dependencies on the name resource. The dependency chain starts with the "Exchange System Attendant" (SA) resource, so the solution is to create a new (temporary) network name and set SA's dependency to the new name resource and remove the  dependency on the nonoperational one.  Now the name resource can be safely deleted without side effects. The next step is to recreate the original name resource (using Cluster Administrator) with all the original settings and re-establish  the original dependency for SA.
Step 3 - bring the virtual Exchange server online
After following the above procedure the virtual Exchange server was easily brought back online. Since all data files were intact no other steps were necessary.