{"id":554,"date":"2009-09-21T09:31:19","date_gmt":"2009-09-21T09:31:19","guid":{"rendered":"https:\/\/www.wapshere.com\/missmiis\/?p=554"},"modified":"2009-11-18T08:38:59","modified_gmt":"2009-11-18T08:38:59","slug":"creating-user-home-directories-windows-version","status":"publish","type":"post","link":"https:\/\/www.wapshere.com\/missmiis\/creating-user-home-directories-windows-version","title":{"rendered":"Creating user home directories &#8211; Windows version"},"content":{"rendered":"<p>I last blogged about provisioning home directories such a long time ago that I talked about <a href=\"https:\/\/www.wapshere.com\/missmiis\/creating-an-extensible-ma\">Netware<\/a>. I also used a SQL table alongside to keep track of a status field as I was doing some end-of-life management &#8211; zipping up the folder and stowing it in an archive location.<\/p>\n<p>But we don&#8217;t need to be that fancy. If all you want is to create a regular Windows-type home folder, at the same time as you create the AD user account, then here&#8217;s a way to do with an XMA.<br \/>\n<!--more--><\/p>\n<h2>Overview<\/h2>\n<p>To summarise the approach:<\/p>\n<ol>\n<li>Create the user account in AD,\n<ul>\n<li>Flow the SID back into the metaverse &#8211; proof the account exists,<\/li>\n<\/ul>\n<\/li>\n<li>Create the home folder using an XMA,\n<ul>\n<li>Flow the path back into the metaverse,<\/li>\n<\/ul>\n<\/li>\n<li>Flow the path out to the user&#8217;s homeDirectory attribute through the AD MA.<\/li>\n<\/ol>\n<h2>Metaverse Schema<\/h2>\n<p>Add the following two attributes to the &#8220;person&#8221; type (or whatever you are using for your user objects):<\/p>\n<ul>\n<li>userSid (Binary)<\/li>\n<li>homeDirectory (String)<\/li>\n<\/ul>\n<h2>Create the XMA<\/h2>\n<p>Create an Extensible MA and call it &#8220;HomeFolders&#8221;. (Note: I don&#8217;t use spaces in my MA names so I have no scripting hassles.)<\/p>\n<h3>Configure Connection Information<\/h3>\n<p>Choose &#8220;Import and Export&#8221; and &#8220;Call based&#8221;.<\/p>\n<p>Connected data source extension: &#8220;HomeFolders_CSExtension.dll&#8221;\u00c2\u00a0 (we will create the extension later)<\/p>\n<p>Connection information: If you want to use this you will have to work out how to use the logon details in your CSExtension code to force the Import and Export steps to use this account instead of the ILM service account. I&#8217;m no great programmer, and to this day I&#8217;ve always managed to get away with giving the ILM service account the permissions I need. In this case it needs permission to create home folders and set ACLs.<\/p>\n<h3>Configure Additional Parameters<\/h3>\n<p>None<\/p>\n<h3>Select Template Input File<\/h3>\n<p>I created a text file called homefolders.txt and saved it to the MaData folder. In the file I have the following line:<\/p>\n<p><code>path,folder,server,owner<\/code><\/p>\n<p>Select the file and choose &#8220;Delimited&#8221; as the file type.<\/p>\n<p>It is useful to keep this template file. Later, if you need to change the schema, you can modify this file and use it in the Refresh Schema operation.<\/p>\n<h3>Delimited Text Format<\/h3>\n<p>Tick &#8220;Use first row for header names&#8221;.<\/p>\n<p>Select &#8220;Comma&#8221; as the delimiter.<\/p>\n<h3>Configure Attributes<\/h3>\n<p>Set Anchor: path<\/p>\n<p>I also click Advanced and change the Fixed object type to &#8220;folder&#8221;.<\/p>\n<h3>Define Object Types<\/h3>\n<p>Leave on the default &#8211; only path is required.<\/p>\n<h3>Connector Filter<\/h3>\n<p>None. Use this if there are certain folder names you want to exclude.<\/p>\n<h3>Join and Projection Rules<\/h3>\n<p>If you&#8217;re lucky the folder names will match the sAMAccoutNames and you can join on that. You could also flow homeDirectory back from the AD MA and join on the path.<\/p>\n<p>Try very hard to avoid manual joins in such an MA as you have no real way to flow an identifier back out to the folder itself (something you should <em>always<\/em> do if manual joins have been used &#8211; you should always be able to clear and re-import a connector space). If you have to go the manual join way then consider using a SQL table alongside this MA, as in <a href=\"https:\/\/www.wapshere.com\/missmiis\/creating-an-extensible-ma\">this post<\/a>, to keep track of folder owners.<\/p>\n<h3>Attribute Flow<\/h3>\n<p>One inbound flow only:<\/p>\n<p><code>path --&gt; homeDirectory<\/code><\/p>\n<h3>Configure Deprovisioning<\/h3>\n<p>&#8220;Make them disconnectors&#8221;<\/p>\n<p>You can delete home folders with an XMA &#8211; you just need to add the appropriate code in your CSExtension to deal with modificationType = delete. But for this example I&#8217;m just creating the folder.<\/p>\n<h3>Configure Extensions<\/h3>\n<p>Click <strong>Finish<\/strong> to create the MA.<\/p>\n<h2>Write the CSExtension<\/h2>\n<p>With the new MA selected, click on <strong>Create Extension Project<\/strong>. In Project type choose &#8220;Connected Data Source Extension&#8221; and then enter as the name &#8220;HomeFolders_CSExtension&#8221;. Make sure your Project Location is correct and then click <strong>OK<\/strong>.<\/p>\n<h3>GenerateImportFile<\/h3>\n<p>The first step is to write the Import step. This should go out and find all the existing home folders and then populate a csv file along the lines of the template you created above. The actual import into the connector space then occurs from the csv file.<\/p>\n<p>Note that my GetOwner function is a fudge &#8211; I actually just return the first domain account that I find with rights to the folder. This is acceptable to me because <em>I am not going to use this MA to modify existing rights<\/em>. All I really care about is checking that the user was correctly assigned to a newly created home folder. Typically the user will be the only domain account with explicit (as opposed to inherited) rights to the folder, so this should work fine.<\/p>\n<pre>  Dim Servers As String() = {\"server1\", \"server2\", \"server3\"}\r\n\r\n    Public Sub GenerateImportFile(ByVal filename As String, ByVal connectTo As String, ByVal user As String, ByVal password As String, ByVal configParameters As ConfigParameterCollection, ByVal fullImport As Boolean, ByVal types As TypeDescriptionCollection, ByRef customData As String) Implements IMAExtensibleFileImport.GenerateImportFile\r\n        Dim server As String\r\n        Dim path As String\r\n        Dim folders As System.Collections.IEnumerator\r\n        Dim fw As StreamWriter\r\n\r\n        Try\r\n            'Open the output file specified in the run profile\r\n            fw = New System.IO.StreamWriter(filename, False)\r\n            fw.WriteLine(\"path,folder,server,owner\")\r\n        Catch ex As Exception\r\n            Throw New UnauthorizedAccessException(\"Unable to open file: \" &amp; filename)\r\n        End Try\r\n\r\n        For Each server In Servers\r\n            folders = Directory.GetDirectories(\"\\\\\" &amp; server &amp; \"\\homedir\").GetEnumerator\r\n            While folders.MoveNext\r\n                path = folders.Current.ToString\r\n                'For each folder, write an entry into the import file\r\n                fw.WriteLine(path &amp; \",\" _\r\n                            &amp; path.Split(\"\\\".ToCharArray)(4) &amp; \",\" _\r\n                            &amp; path.Split(\"\\\".ToCharArray)(2) &amp; \",\" _\r\n                            &amp; GetOwner(path))\r\n            End While\r\n        Next\r\n        fw.Close()\r\n    End Sub\r\n\r\n   Function GetOwner(ByVal path As String) As String\r\n        ' This function does not get the actual owner of the folder,\r\n        ' rather it returns the first domain trustee account.\r\n        ' This is not very precise but the purpose is just to check the ACL\r\n        ' for new folders so it will do.\r\n        Dim AccessRules As AuthorizationRuleCollection\r\n        Dim AuthRule As AuthorizationRule = Nothing\r\n        Dim Owner As String = \"\"\r\n        Dim Fs As FileSecurity\r\n\r\n        Try\r\n            Fs = New FileSecurity(path, AccessControlSections.Access)\r\n            AccessRules = Fs.GetAccessRules(True, False, Type.GetType(\"System.Security.Principal.NTAccount\"))\r\n            If Not AccessRules Is Nothing Then\r\n                For Each AuthRule In AccessRules\r\n                    If AuthRule.IdentityReference.ToString.IndexOf(\"MYDOMAIN\\\") = 0 Then\r\n                        Owner = AuthRule.IdentityReference.ToString\r\n                        Exit For\r\n                    End If\r\n                Next\r\n            End If\r\n        Catch ex As Exception\r\n        End Try\r\n\r\n        If AuthRule Is Nothing Or _\r\n           Owner = \"\" Or _\r\n           Owner.IndexOf(\"MYDOMAIN\\\") &lt; 0 Then\r\n            Return \"\"\r\n        Else\r\n            Return Owner\r\n        End If\r\n\r\n    End Function<\/pre>\n<p>Once this code is in place you should be able to run an Import step on your MA. You will need to specify an import file name &#8211; just something like &#8220;import.csv&#8221; is fine. Once your code is working you will be able to see the file created in the MaData\\HomeFolders directory, and populated with information about the folders it has found.<\/p>\n<h3>ExportEntry<\/h3>\n<p>I&#8217;m going to include the code for the ExportEntry sub here, but we won&#8217;t have anything to export until we modify our MVExtension provisioning code to create new folder objects. More on that below.<\/p>\n<pre>    Public Sub ExportEntry(ByVal modificationType As ModificationType, ByVal changedAttributes As String(), ByVal csentry As CSEntry) Implements IMAExtensibleCallExport.ExportEntry\r\n\r\n        If modificationType = Microsoft.MetadirectoryServices.ModificationType.Add Then\r\n            Directory.CreateDirectory(csentry(\"path\").StringValue)\r\n\r\n            If Directory.Exists(csentry(\"path\").StringValue) Then\r\n                Try\r\n                    ' Get current ACL from the folder\r\n                    Dim security As DirectorySecurity = Directory.GetAccessControl(csentry(\"path\").StringValue)\r\n\r\n                    ' Create a new rule granting owner full access\r\n                    Dim rule As New FileSystemAccessRule(New NTAccount(csentry(\"owner\").StringValue.ToLower), FileSystemRights.FullControl, AccessControlType.Allow)\r\n\r\n                    ' Add the rule to the existing rules\r\n                    security.AddAccessRule(rule)\r\n\r\n                    ' Persist the changes\r\n                    Directory.SetAccessControl(csentry(\"path\").StringValue, security)\r\n\r\n                Catch ex As Exception\r\n                    ' If the ACL failed the delete the folder so the operation can be retried later.\r\n                    Directory.Delete(csentry(\"path\").StringValue)\r\n                    Throw New EntryExportException(\"Failed to set ACL on \" &amp; csentry(\"path\").StringValue _\r\n                              &amp; \" - deleting folder. \" &amp; vbCrLf &amp; ex.Message)\r\n                End Try\r\n\r\n            End If\r\n        End If\r\n\r\n    End Sub<\/pre>\n<h2>Provisioning<\/h2>\n<p>Now we want to create the home folder.<\/p>\n<p>As I indicated at the top of this post, you need some kind of proof that the user account was created before you create the home folder. The reason is permissions &#8211; you can&#8217;t grant permissions to a user that doesn&#8217;t exist yet. Technically you could provision your user and home folder objects at the same time, so long as you made sure the AD MA was exported first, but fundamentally I don&#8217;t like this. Even though it means running a series of export\/import-syncs in a row, I much prefer to wait for the user account before I provision the home folder.<\/p>\n<p>And the way I do this is by checking the Sid. I flow the userSid back into the Metaverse from the AD MA and use this as a test in my provisioning logic for home folders.<\/p>\n<pre>  Sub HomeFolder_Provisioning(ByVal mventry As MVEntry)\r\n        Dim HFMA As ConnectedMA = mventry.ConnectedMAs(\"HomeFolders\")\r\n        Dim path As String = \"\"\r\n        Dim server As String = \"\"\r\n\r\n        '' Generate the path based on location\r\n        If mventry(\"location\").IsPresent Then\r\n            Select Case mventry(\"location\").StringValue.ToLower\r\n                Case \"london\"\r\n                    server = \"server1\"\r\n                Case \"z\u00c3\u00bcrich\"\r\n                    location = \"server 2\"\r\n                Case Else\r\n                    server = \"server3\"\r\n            End Select\r\n            path = \"\\\\\" &amp; server &amp; \"\\homedir\\\" &amp; mventry(\"uid\").StringValue\r\n        End If\r\n\r\n        '' Should the folder exist? AD account must be provisioned first.\r\n        '' Note: Folders are NOT deleted. ShouldExist = FALSE just prevents creation.\r\n        If mventry(\"Sid_AD\").IsPresent Then\r\n            ShouldExist = True\r\n        Else\r\n            ShouldExist = False\r\n        End If\r\n\r\n        '' Check if the folder already exists\r\n        Select Case HFMA.Connectors.Count\r\n            Case 0\r\n                DoesExist = False\r\n            Case 1\r\n                DoesExist = True\r\n            Case Else\r\n                Throw New UnexpectedDataException(\"The metaverse person object with employeeID =\" _\r\n                 &amp; mventry(\"employeeID\").StringValue &amp; \" has more than one connector in MA \" &amp; HFMA.Name)\r\n        End Select\r\n\r\n        '' Take action based on values of ShouldExist and DoesExist\r\n        If ShouldExist And DoesExist Then\r\n            'Do nothing\r\n\r\n        ElseIf ShouldExist And Not DoesExist Then\r\n            'Create folder object in CS\r\n            Dim csentry As CSEntry = HFMA.Connectors.StartNewConnector(\"folder\")\r\n            csentry(\"path\").Value = path\r\n            csentry(\"server\").Value = server\r\n            csentry(\"folder\").Value = mventry(\"uid\").StringValue\r\n            csentry(\"owner\").Value = \"MYDOMAIN\\\" &amp; mventry(\"uid\").StringValue\r\n            csentry.CommitNewConnector()\r\n\r\n        ElseIf Not ShouldExist And DoesExist Then\r\n            'Do nothing\r\n\r\n        Else 'Should not exist and does not exist\r\n            'Do nothing\r\n        End If\r\n\r\n    End Sub<\/pre>\n<p>All going well, you should now be able to Sync your AD MA, and any AD accounts that are missing a home folder should have one provisioned.<\/p>\n<p>Start testing your exports by restricting the Export step to only process one object at a time.<\/p>\n<h2>Update User homeDirectory<\/h2>\n<p>The final step is to flow the path out to the homeDirectory attribute on the user, using the AD MA.<\/p>\n<p><code>homeDirectory --&gt; homeDirectory<\/code><\/p>\n<p>If you want to map a particulay drive letter at the same time then flow a constant to homeDrive, eg.,<\/p>\n<p><code>\"H:\" --&gt; homeDrive<\/code><\/p>\n<h2>Doing more<\/h2>\n<p>You can do more. As I mentioned above you could modify your ExportEntry sub to process a delete, and then delete the home folder &#8211; though you would want to tread very carefully. There are bound to be far more interesting things you can do with the permissions, or with sharing the folder &#8211; but I&#8217;ll leave that for you to work out. Unless I have to do something like that myself &#8211; and then I will no doubt write it up here.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I last blogged about provisioning home directories such a long time ago that I talked about Netware. I also used a SQL table alongside to keep track of a status field as I was doing some end-of-life management &#8211; zipping up the folder and stowing it in an archive location. But we don&#8217;t need to&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"footnotes":"","jetpack_publicize_message":"","jetpack_is_tweetstorm":false,"jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":[]},"categories":[24,34],"tags":[],"class_list":["post-554","post","type-post","status-publish","format-standard","hentry","category-ad","category-ilm2007"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_shortlink":"https:\/\/wp.me\/pkp1o-8W","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/posts\/554","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/comments?post=554"}],"version-history":[{"count":8,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/posts\/554\/revisions"}],"predecessor-version":[{"id":600,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/posts\/554\/revisions\/600"}],"wp:attachment":[{"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/media?parent=554"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/categories?post=554"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/tags?post=554"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}