Click here to Skip to main content
11,642,216 members (59,736 online)
Click here to Skip to main content

Disable Local Workspaces in TFS 2013

, 9 Mar 2014 CPOL 4K 2
Rate this:
Please Sign up or sign in to vote.
CodeProject table.customTable1 { width: 90%; border-collapse: collapse; margin-left: 5px; margin-right: 5px; } .customTable1 thead { background-color: #e8e8e9; } .customTable1 td { border: 1px solid #979797; padding-left: 4px; padding-right: 4px; padding-bot

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 plugin 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 plugin 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 plugin 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 plugin that you may use in your status message output.

The Priority property controls the order in which the server invokes plugins. A plugin with a higher priority is invoked first. If the ProcessEvent(...) call for a given plugin 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 Type DecisionPoint Notification
CheckinNotification Yes Yes
PendChangesNotification Yes Yes
UndoPendingChangesNotification Yes Yes
ShelvesetNotification Yes Yes
ShelvesetNotification Yes Yes
WorkspaceNotification Yes Yes
LabelNotification No Yes
CodeChurnCompletedNotification No Yes

Build Events (Microsoft.TeamFoundation.Build.Server.dll)
Event Type DecisionPoint Notification
BuildCompletionEvent No Yes
BuildQualityChangedNotificationEvent No Yes

Work Item Tracking Events (Microsoft.TeamFoundation.WorkItemTracking.Server.dll)
Event Type DecisionPoint Notification
WorkItemChangedEvent No Yes
WorkItemMetadataChangedNotification No Yes
WorkItemsDestroyedNotification No Yes
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Jeremy A. Wilson
Software Developer
United States United States
I've worked in the industry since 1992. After the dot com crash of 2001, I went back to school and graduated from the University of North Texas in 2005. I now live in the Dallas area with my wife and two children, and work as a senior software engineer for a local company.

I first learned to program on a Commodore 64 when I was 12 years old. The rest is history...

You may also be interested in...

Comments and Discussions

 
QuestionCreative way to do a horrible thing Pin
jtmueller14-May-14 11:03
memberjtmueller14-May-14 11:03 
AnswerRe: Creative way to do a horrible thing Pin
jerhewet1-Apr-15 4:01
memberjerhewet1-Apr-15 4:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.150731.1 | Last Updated 10 Mar 2014
Article Copyright 2014 by Jeremy A. Wilson
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid