{"id":176,"date":"2008-09-10T09:30:43","date_gmt":"2008-09-10T09:30:43","guid":{"rendered":"https:\/\/www.wapshere.com\/missmiis\/?p=176"},"modified":"2009-11-18T08:41:04","modified_gmt":"2009-11-18T08:41:04","slug":"disable-delete","status":"publish","type":"post","link":"https:\/\/www.wapshere.com\/missmiis\/disable-delete","title":{"rendered":"Disable &#8211; Delete"},"content":{"rendered":"<p>A common requirement is that user accounts should go through a disabled stage of some length before being deleted. This makes excellent sense, particularly in AD with its fastidiousness concerning SIDs.<\/p>\n<p>In this post I outline a way to achieve this in AD using a datestamped attribute, export flow rules and provisioning code.<\/p>\n<p><!--more--><\/p>\n<h3>The General Approach<\/h3>\n<p>For this example I use the <strong>info<\/strong> attribute in AD to keep a record of the latest change, for example &#8220;Disabled on 12\/10\/2008&#8221;.<\/p>\n<p>Say we are retaining disabled accounts for 90 days &#8211; once the provisioning code sees that the Disabled\u00c2\u00a0date is more than 90 days in the past, and as long as the account is still actually disabled, the account will be deprovisioned.<\/p>\n<h3>Disabling the Account<\/h3>\n<p>Now firstly I am assuming you have some kind of <strong>status<\/strong> attribute in the metaverse which has just been changed to &#8220;inactive&#8221;. You feed this into an\u00c2\u00a0export flow rule\u00c2\u00a0which updates\u00c2\u00a0the userAccountControl attribute.<\/p>\n<pre>Case \"export_userAccountControl\"\r\n    Const ADS_UF_ACCOUNTDISABLE As Integer = &amp;H2\r\n    Const ADS_UF_NORMAL_ACCOUNT As Integer = &amp;H200\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0Dim currentValue As Long\r\n\r\n \u00c2\u00a0\u00c2\u00a0 If csentry(\"userAccountControl\").IsPresent Then\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0currentValue = csentry(\"userAccountControl\").IntegerValue\r\n \u00c2\u00a0\u00c2\u00a0 Else\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0currentValue = ADS_UF_NORMAL_ACCOUNT\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 End If\r\n\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 Select Case mventry(\"status\").Value\r\n\u00c2\u00a0 \u00c2\u00a0\u00c2\u00a0   Case \"active\"\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0    csentry(\"userAccountControl\").IntegerValue = (currentValue Or ADS_UF_NORMAL_ACCOUNT) _\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 And (Not ADS_UF_ACCOUNTDISABLE)\r\n\u00c2\u00a0 \u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 Case \"inactive\"\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 csentry(\"userAccountControl\").IntegerValue = currentValue _\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 Or ADS_UF_ACCOUNTDISABLE\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 End Select\u00c2\u00a0<\/pre>\n<h3>Moving the Account<\/h3>\n<p>A lot of people like to keep the disabled accounts in a seperate OU. This is a straight-forward case of determining the OU in your provisioning code based on the status, and then renaming the DN if necessary.<\/p>\n<p>The following snippet is not a complete provision sub, but shows the code you need to work in to get the accounts moved.<\/p>\n<pre>    Const ACTIVE_OU = \"OU=Users, dc=mydomain, dc=com\"\r\n    Const INAVTIVE_OU = \"OU=Disabled, OU=Users, dc=mydomain, dc=com\"\r\n    Const MA_NAME = \"AD\"\r\n    Dim MA As ConnectedMA = mventry.ConnectedMAs(MA_NAME)\r\n    Dim ShouldExist as Boolean  'Not shown here - some sort of logic to work out this value \r\n\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0'Generate the expected DN\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0Dim RDN As String = \"CN=\" &amp; mventry(\"displayName\").Value\r\n \u00c2\u00a0\u00c2\u00a0\u00c2\u00a0Dim Container As String\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0Select Case mventry(\"status\").Value.ToLower\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 Case \"active\"\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 Container = ACTIVE_OU\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 Case \"inactive\"\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 Container = INACTIVE_OU\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 Case Else\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 Throw New UnexpectedDataException(\"User Status \" &amp; mventry(\"status\").Value &amp; _\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 \" is not supported for user provisioning in \" &amp; MA_NAME)\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 Exit Sub\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 End Select\r\n\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 Dim DN As ReferenceValue = MA.EscapeDNComponent(RDN).Concat(Container)<\/pre>\n<pre>    If ShouldExist and MA.Connectors.Count = 1 Then\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 'Check if rename needed\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 Dim CSEntry As CSEntry = MA.Connectors.ByIndex(0)\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 If CSEntry.DN.ToString.ToLower &lt;&gt; DN.ToString.ToLower Then\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 CSEntry.DN = DN\r\n\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0\u00c2\u00a0 End If\r\n    End If<\/pre>\n<h3>Recording the Disabled Date<\/h3>\n<p>Now, this is where it gets a little tricky.<\/p>\n<p>I want to update the <strong>info<\/strong> attribute with the date the account was disabled. You could base this on <strong>status<\/strong> however the change in status alone does not prove the account was actually disabled. I prefer to base this change on the hard evidence of the userAccountControl attribute &#8211; the downside being that I have to flow userAccountControl back in before I can update info &#8211; that is, the info will be updated on <em>the sync after<\/em> the account was actually disabled.<\/p>\n<p>So, create a metaverse attribute for userAccountControl and then create a direct Import flow rule to flow it into the metaverse.<\/p>\n<p>Next, create an Advanced export flow rule for info, as follows.<\/p>\n<pre>Case \"export_info\"\r\n    'AD userAccountControl must have been flowed into metaverse\r\n    'Info is updated on the sync following userAccountControl being changed.\r\n    If mventry(\"userAccountControl\").IsPresent AndAlso _\r\n      (mventry(\"userAccountControl\").IntegerValue And ADS_UF_ACCOUNTDISABLE) = ADS_UF_ACCOUNTDISABLE Then\r\n        'Account is disabled\r\n        If Not csentry(\"info\").Value.Contains(\"Disabled\") Then\r\n            csentry(\"info\").Value = \"Disabled on \" + Today.ToString(\"d\")\r\n        End If\r\n    Else\r\n        'Account is enabled\r\n        If csentry(\"info\").Value.Contains(\"Disabled\") Then\r\n            csentry(\"info\").Value = \"Enabled on \" + Today.ToString(\"d\")\r\n        End If\r\n    End If<\/pre>\n<h3>And finally, the Deletion<\/h3>\n<p>So, we now should have accounts successfully disabled and tagged with the date it happened.<\/p>\n<p>The final step is to add the Deprovisioning instructions into the provisioning code &#8211; and for this you are going to need that <strong>info<\/strong> attribute in the metaverse as well. So create another metaverse attribute, and the corresponding Direct import flow rule.<\/p>\n<p>Now you can update your provisioning code to decide when the time is right to delete the account. Note in the provisioning snippet above I made use of a boolean called ShouldExist. When I have determined that ShouldExist is FALSE I can call my csentry.Deprovision method. The code will be something like this:<\/p>\n<pre>Select Case mventry(\"status\").Value.ToLower\r\n    Case \"active\"\r\n        ShouldExist = True\r\n\r\n    Case \"inactive\"\r\n        ShouldExist = False\r\n        If mventry(\"userAccountControl\").IsPresent AndAlso _\r\n           mventry(\"info\").IsPresent Then\r\n            If (mventry(\"userAccountControl\").IntegerValue And ADS_UF_ACCOUNTDISABLE) = ADS_UF_ACCOUNTDISABLE Then\r\n                'Account disabled\r\n                ShouldExist = True\r\n                    If mventry(\"description\").Value.Contains(\"Disabled\") Then\r\n                        Dim strDate As String = mventry(\"description\").Value.Replace(\"Disabled on \", \"\")\r\n                        Dim disabledDate As DateTime = Convert.ToDateTime(strDate)\r\n                        If Now.Subtract(disabledDate).Days &gt; KEEP_DISABLED_DAYS Then\r\n                            ShouldExist = False\r\n                        End If\r\n                    End If\r\n            Else\r\n                'Account enabled\r\n                ShouldExist = True\r\n            End If\r\n         End If\r\n\r\n    Case Else\r\n         Throw New UnexpectedDataException(\"Unexpected User Status \" &amp; mventry(\"status\").Value)\r\n         Exit Sub\r\nEnd Select\r\n.\r\n.\r\n.\r\nIf Not ShouldExist And MA.Connectors.Count = 1 Then\r\n    Dim CSEntry As CSEntry = MA.Connectors.ByIndex(0)\r\n    CSEntry.Deprovision()\r\nEnd If<\/pre>\n<h3>Downsides to this approach<\/h3>\n<p>The main weakness I would confess to is that the system can be effected by changes made directly in AD. If someone changes the date in the info field the account may be deleted sooner or later &#8211; though I have made good use of this as a simple way to extend the life of a particular account. The main problem would occur if someone deleted all text from the info field, leaving MIIS with no information. If you need a system that is entirely separated from this AD dependance then I would recommend an extra SQL MA to keep track of the dates and statuses.<\/p>\n<p>The other potential concern is the fact that the info attribute is updated on the second sync. This is fine if you&#8217;re happy to just have a date in the field &#8211; not so fine if you want an exact time. You can minimise the problem by running two Export\/DIDS operations in a row, but even then the time won&#8217;t be exact. Unfortunately I&#8217;ve never found a great solution to whole description-updating issue &#8211; for more on that see <a href=\"https:\/\/www.wapshere.com\/missmiis\/?p=48\">this post<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A common requirement is that user accounts should go through a disabled stage of some length before being deleted. This makes excellent sense, particularly in AD with its fastidiousness concerning SIDs. In this post I outline a way to achieve this in AD using a datestamped attribute, export flow rules and provisioning code.<\/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":[34,28],"tags":[],"class_list":["post-176","post","type-post","status-publish","format-standard","hentry","category-ilm2007","category-miis2003"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_shortlink":"https:\/\/wp.me\/pkp1o-2Q","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/posts\/176","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=176"}],"version-history":[{"count":6,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/posts\/176\/revisions"}],"predecessor-version":[{"id":605,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/posts\/176\/revisions\/605"}],"wp:attachment":[{"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/media?parent=176"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/categories?post=176"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/tags?post=176"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}