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…
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…
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>).
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( _
"http://schemas.microsoft.com/TeamFoundation/2005" & _
"/06/Services/Notification/03/Notify", _
RequestNamespace:="http://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
' http://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
'<http://geekswithblogs.net/ranganh/archive/2006/02/21/70212.aspx>
GCI.PreAuthenticate = True
GCI.Credentials = System.Net.CredentialCache.DefaultCredentials
'</http://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
' http://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