ACL and Group Membership change logging in TFS – what are your options?

As you may know, current versions of TFS do not offer any native method to log security changes (this story is not changing much in TFS 2010, either).  So what are your options for tracking TFS security changes now? Well, I’d like to present a few ideas based on some discussions I’ve seen internally this week…

  • Manage your TFS security in Active Directory

    I have not researched it but I am fairly certain there have to be at least two AD monitoring tools out there that allow admins to track changes therein. We have long recommended that TFS group membership be managed through active directory groups, so once those groups are created and added to TFS, you maintain membership through Active Directory Users & Computer, or some other tool – not in TFS itself.

  • Roll Your Own

    TFS is open and very extensible. It’s also basically a bunch of web services. Now that my be a huge oversimplification (as I’m sure anyone who’s ever supported it to any degree will attest), but the fact remains – the client interface to TFS and the intercommunication of TFS to itself is all handled via web services. As such, you are – for the most part, and given the proper rights – allowed to call these same web services for your own purposes. To that end may I present the IdentityChangedEvent, AclChangedEvent and DataChangedEvent TFS events. The first two were introduced in TFS 2005 and later deprecated by the third (DataChangedEvent) with the release of 2008. I won’t go into depth on their usage here, suffice it to say that these events should provide enough information to those wanting to monitor TFS ACL and group membership changes through their own home-grown apps. Brian Randell's MSDN magazine article on the TFS Eventing Service is a good reference to get you started here. Here is another by Mariano Szklanny. This one has some really cool code showing how to send emails based on certain WIT events, and a particularly useful tip on translating an AD user name into their email address!

Try as I might I was unable to find an example on the Interwebs of subscribing to/utilizing information from the DataChangedEvent. So, using the aforementioned samples in bullet two as a base, I created this simple one below. Basically it’s a web service which subscribes to DataChangedEvent on a TF server. It receives the XML from the event and if the  DataType == IDENTITY, passes the SeqId-1 to the GetChangedIdentities method of  GroupSecurityService2.asmx on the same server. It then writes what it gets back to a custom even log. In my testing it is triggered very well by adding a Windows user to a TFS group (for example). Once created and up on a web server - be it on the TFS AT itself or another IIS server - you will have to subscribe your web service to DataChangedEvent on your TF server. This is the command I used (one line), where…

  • My CMD prompt was open to C:\Program Files\Microsoft Visual Studio 2008 Team Foundation Server\TF Setup on the TF server
  • My TF server was named trevorh-wstfs08
  • My custom listener web service was named ProcessDataChangedEvent.asmx, and was running on the TFS AT… just on another port (51235)

bissubscribe /eventType DataChangedEvent /address trevorh-wstfs08:51235/ProcessDataChangedEvent.asmx /deliveryType Soap /server https://TREVORH-WSTFS08:8081

This is not meant to be production ready by any means, of course. My intent here was to publish an example of how one might capture some of this identity change info from DataChangedEvent. Try it out and have a look in your Event Viewer on the machine hosting the web service. You may be surprised at the info you can get out of GetChangedIdentities  as you manipulate TFS permissions. The code is fairly well commented, but if you have any questions please post a comment of your own and I will try to help you out (no slagging my coding skills, please <g>).

Hope this helps!
- Trevor

 Imports System.Web.Services
 Imports System.Web.Services.Protocols
 Imports System.ComponentModel
 Imports System.IO
 Imports System.Security.Cryptography.X509Certificates
 Imports System.Net
 Imports System.Net.Security
  
 <System.Web.Services.WebService(Namespace:="TrevTools")> _
 <System.Web.Services.WebServiceBinding(ConformsTo:=WsiProfiles.BasicProfile1_1)> _
 <ToolboxItem(False)> _
 Public Class ProcessDataChangedEvent
     Inherits System.Web.Services.WebService
  
     <SoapDocumentMethod( _
   "schemas.microsoft.com/TeamFoundation/2005" & _
   "/06/Services/Notification/03/Notify", _
   RequestNamespace:="schemas.microsoft.com" & _
   "/TeamFoundation/2005/06/Services/Notification/03")> _
   <WebMethod()> _
     Public Sub Notify(ByVal eventXml As String, _
                  ByVal tfsIdentityXml As String)
  
         ' Creates event log for this app. Only need to be run one per machine.
         ' SetupEventSource()
  
         ' Necessary in order to deal with self-served certificate on my TF server using HTTPS
         ' www.xtremevbtalk.com/showthread.php?p=1342414#post1342414
         ServicePointManager.ServerCertificateValidationCallback = _
             New RemoteCertificateValidationCallback(AddressOf CertificateValidationCallBack)
  
         Const logSource As String = "TFS_DataChangedEvent_Listener"
         Const logApp As String = "ProcessDataChangedEvent"
         Dim el As New EventLog(logApp)
         el.Source = logSource
  
         Try
             If eventXml Is Nothing OrElse eventXml = String.Empty Then Return
             Dim x As XElement = XElement.Load(New StringReader(eventXml))
  
             Dim DataType As String = x.<DataType>.Value
             If DataType Is Nothing OrElse DataType = String.Empty Then
                 el.WriteEntry("DataType was empty")
                 Return
             End If
  
             Dim SeqId As String = x.<SeqId>.Value
             If SeqId Is Nothing OrElse SeqId = String.Empty Then
                 el.WriteEntry("SeqId was empty")
                 Return
             End If
  
             el.WriteEntry("DataType " & DataType & ", SeqId " & _
                         SeqId & " received. Next event log contains data...")
  
  
             ' In my testing the SeqId returned is the next number that will be used (MaxSequence), 
             ' not the one that we're interested in, so subtract 1 from it before calling GetChangedIdentities()
             ' from /Services/v2.0/GroupSecurityService2.asmx
             SeqId = CType((CType(SeqId, Integer) - 1), String)
  
             If DataType = "IDENTITY" Then
                 ' "MyTFS" is a Web Reference to https://trevorh-wstfs08:8081/Services/v2.0/GroupSecurityService2.asmx
                 Dim GCI As New MyTFS.GroupSecurityService2
                 '<geekswithblogs.net/ranganh/archive/2006/02/21/70212.aspx>
                 GCI.PreAuthenticate = True
                 GCI.Credentials = System.Net.CredentialCache.DefaultCredentials
                 '</geekswithblogs.net/ranganh/archive/2006/02/21/70212.aspx>
                 Dim ChangeInfo As String = GCI.GetChangedIdentities(SeqId)
                 If ChangeInfo Is Nothing OrElse ChangeInfo = String.Empty Then
                     Return
                 Else
                     el.WriteEntry(ChangeInfo)
                 End If
             End If
  
         Catch ex As Exception
             el.WriteEntry(ex.Message, EventLogEntryType.Error)
         End Try
  
     End Sub
  
     ' Creates an event log. Make sure you have enough permission to do this.
     Private Sub SetupEventSource()
         Const logSource As String = "TFS_DataChangedEvent_Listener"
         Const logApp As String = "ProcessDataChangedEvent"
  
         If Not EventLog.SourceExists(logSource) Then
             EventLog.CreateEventSource(logSource, logApp)
         End If
     End Sub
  
     ' Necessary in order to deal with self-served certificate on my TF server using HTTPS
     ' www.xtremevbtalk.com/showthread.php?p=1342414#post1342414
     Function CertificateValidationCallBack( _
         ByVal sender As Object, _
         ByVal certificate As X509Certificate, _
         ByVal chain As X509Chain, _
         ByVal sslPolicyErrors As SslPolicyErrors _
             ) As Boolean
         Return True
     End Function
  
 End Class