Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Disable Local Workspaces in TFS 2013

0.00/5 (No votes)
9 Mar 2014 1  
How to disable local workspaces in TFS 2013

Disclaimer

This article describes something that can be done to control TFS 2013 from the server, not necessarily what should be done. The technique described though may give life to possibilities not previously considered.

The Problem

I had a client who, because of auditing requirements on their code base, needed the ability to audit checking out of source code files at the point of check-out and prevent check-out under certain custom conditions.

Such policies are difficult to enforce in TFS 2013 because contributors are allowed to setup local workspaces for their development environment. With a local workspace, files may be checked out without the server being notified. The server isn't notified of file changes until it receives a “Pend Changes” request upon attempting to check-in changed files.

With server-side workspaces however, the server is notified whenever a version-controlled action is attempted. But, (so far as I am aware) the administration console for TFS does not provide a facility for controlling whether or not contributors can create local workspaces, or change an existing workspace to a local workspace.

The Solution

Fortunately, TFS does allow some custom plug in integration through use of the ISubscriber interface. To integrate with the TFS server, we create a standard class library assembly with one or more classes that implement this interface. Through integration with our assembly, we can not only listen for certain events, but also allow or deny those events based on whatever custom logic we provide.

Setting Up Your Project

In order to create a plug in assembly for TFS, you must install the TFS Server software on your local development machine. This not only installs the assemblies your project will need to reference, but is also necessary to debug your code.

Once installed, create a standard class library project and add a reference to the following list of assemblies, which are all located at under the directory [TFS Install Directory]\Application Tier\Web Services\bin\.

  • Microsoft.TeamFoundation.Common.dll
  • Microsoft.TeamFoundation.Framework.Server.dll
  • Microsoft.TeamFoundation.Server.Core.dll
  • Microsoft.TeamFoundation.VersionControl.Server.dll

Several other assemblies reside in this directory as well, some of which you will also likely need to reference if you want your plug in to respond to other types of events.

The ISubscriber Interface

The interface is rather simple, containing only two properties and two functions:

public interface ISubscriber {
    string Name { get; }

    SubscriberPriority Priority { get; }

    EventNotificationStatus ProcessEvent(TeamFoundationRequestContext requestContext,
                NotificationType notificationType, object notificationEventArgs,
                out int statusCode, out string statusMessage, 
                out ExceptionPropertyCollection properties);

    Type[] SubscribedTypes();
}   

The Name property is a placeholder for identifying the plug in that you may use in your status message output.

The Priority property controls the order in which the server invokes plugins. A plug in with a higher priority is invoked first. If the ProcessEvent(...) call for a given plug in returns a value of EventNotificationStatus.ActionDenied, subsequent plugins with a lower priority will not be invoked.

The server invokes the ProcessEvent(...) function when an event to which the class is subscribed occurs. In fact, the server often (but not always) invokes the method twice for a given event, once with notificationType = NotificationType.DecisionPoint before the server performs the requested operation, and again with notificationType = NotificationType.Notification after the operation has been performed. See Table 1 for more details.

The server calls the SubscribedTypes() function expecting it to return an array of types that represent events for which the server should call the ProcessEvent(...) function. If this function does not return the appropriate type for a given event, then the server will not call the ProcessEvent(...) function when that event occurs. See Table 1 for more details.

Version Control Events (Microsoft.TeamFoundation.VersionControl.Server.dll)
Event TypeDecisionPointNotification
CheckinNotificationYesYes
PendChangesNotificationYesYes
UndoPendingChangesNotificationYesYes
ShelvesetNotificationYesYes
ShelvesetNotificationYesYes
WorkspaceNotificationYesYes
LabelNotificationNoYes
CodeChurnCompletedNotificationNoYes

 

Build Events (Microsoft.TeamFoundation.Build.Server.dll)
Event TypeDecisionPoint Notification
BuildCompletionEventNoYes
BuildQualityChangedNotificationEventNoYes

 

Work Item Tracking Events (Microsoft.TeamFoundation.WorkItemTracking.Server.dll)
Event TypeDecisionPointNotification
WorkItemChangedEventNoYes
WorkItemMetadataChangedNotificationNoYes
WorkItemsDestroyedNotificationNoYes
Table 1

Martin Hinshelwood has put together a more comprehensive list of events at TFS Event Handler for Team Foundation Server 2010 that also covers team build and test management events.

A Roadblock

To accomplish our goal, we need to create a class that implements the ISubscriber interface. Unfortunately, the WorkspaceNotification object does not contain any information about the type of workspace the client is attempting to create or update. In addition, the requestContext parameter passed to the function doesn't contain the information we want either - at least not publicly.

It turns out that the TeamFoundationRequestContext object contains a non-public property that returns the System.Web.HttpContext of the Http request the client sent to the server. With a little reflection voodoo, we can obtain the value of the private HttpContext property and examine its Request.InputStream property, which contains a standard SOAP message similar to the following example. From the XML, we can obtain the information we really do want!

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
    <s:Body>
        <UpdateWorkspace xmlns="http://schemas.microsoft.com/TeamFoundation/
        2005/06/VersionControl/ClientServices/03">
            <oldWorkspaceName>JWILSON5-LT</oldWorkspaceName>
            <ownerName>{...Active Directory User ID...}</ownerName>
            <newWorkspace computer="JWILSON5-LT" islocal="true" name="JWILSON5-LT" 

                ownerdisp="Jeremy Wilson" owner="{...Active Directory User ID...}">
                <Comment/>
                <Folders>
                    <WorkingFolder local="C:\TFS\JWILSON5-LT" item="$/"/>
                </Folders>
                <OwnerAliases>
                    <string>{...Active Directory User ID...}</string>
                    <string>{...TFS User Name...}</string>
                    <string>Jeremy Wilson</string>
                </OwnerAliases>
            </newWorkspace>
            <supportedFeatures>1919</supportedFeatures>
        </UpdateWorkspace>
    </s:Body>
</s:Envelope>  

Code Example

Putting the pieces together, I've come up with the following code example that allows us to receive notifications when a user attempts to create or update a workspace as a local workspace, and then deny the request with an appropriate response:

    public class CustomTfsEventHandler: ISubscriber
    {
        #region ISubscriber Members
 
        public string Name
        {
            get { return "CustomTfsEventHandler"; }
        }
 
        public SubscriberPriority Priority
        {
            get { return SubscriberPriority.Normal; }
        }
 
        public EventNotificationStatus ProcessEvent(TeamFoundationRequestContext requestContext, 
            NotificationType notificationType, object notificationEventArgs, 
            out int statusCode, out string statusMessage, 
            out ExceptionPropertyCollection properties)
        {
            // Set initial state of out parameters
            statusCode = 0;
            properties = null;
            statusMessage = String.Empty;
 
            try
            {
                if (notificationEventArgs is WorkspaceNotification
                    && notificationType == NotificationType.DecisionPoint
                    && new[] { "CreateWorkspace", 
                    "UpdateWorkspace" }.Contains(requestContext.Command))
                {
                    // Implement logic here
                    PropertyInfo propertyInfo = requestContext.GetType().GetProperty("HttpContext", 
                        BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.GetField);
 
                    HttpContext httpContext = (HttpContext)propertyInfo.GetValue(requestContext);
 
                    XmlDocument document = new XmlDocument();
                    httpContext.Request.InputStream.Position = 0;
                    document.Load(httpContext.Request.InputStream);
                    XmlNode node = document.SelectSingleNode
                                   ("s:Envelope/s:Body/UpdateWorkspace/newWorkspace");
                    bool isLocal = Convert.ToBoolean(node.Attributes["islocal"].Value);
 
                    if (isLocal)
                    {
                        statusCode = 2;
                        statusMessage = "Local Workspaces are not allowed for this 
                                         Team Project Collection.";
                        return EventNotificationStatus.ActionDenied;
                    }
                }

                return EventNotificationStatus.ActionPermitted;
            }
            catch(Exception ex)
            {
                statusCode = 1; // Some arbitrary non-zero value
                statusMessage = "RealPage TFS Extension Error:  
                Critical Failure.  An unexpected error has occurred.";
                properties = new ExceptionPropertyCollection();
                properties.Set("Internal Exception", ex.ToString());
                TeamFoundationApplicationCore.LogException
                (requestContext, ex.Message, ex, 1, System.Diagnostics.EventLogEntryType.Error);
 
                return EventNotificationStatus.ActionDenied;
            }
        }
 
        public Type[] SubscribedTypes()
        {
            return new[] { typeof(WorkspaceNotification) };
        }
 
        #endregion
    }

Installation & Debugging

Installation is as simple as copying the resulting assembly to the [TFS Install Directory]\Application Tier\Web Services\bin\Plugins\ directory. TFS will automatically detect the new assembly and start using it.

To debug your assembly, you will need to select Debug -> Attach to process... from your development environment, check the box labeled "Show processes from all users", and select the "w3wp.exe" process.

Conclusion

By digging under the covers of the SOAP messages sent to the TFS server, I've demonstrated how one can gain even more control over TFS. Hopefully, you too will be able to make use of this technique in the future!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here