Improvements to workspaces in TFS 2010

In TFS 2005 and 2008, workspaces in version control have the following limitations.

  1. The owner of a workspace is set at creation time and cannot be changed (immutable).
  2. A workspace can only be used by its owner.

When we speak of “using” a workspace, that covers any of the following sorts of operations:

  • Unshelving a shelveset into the workspace
  • Doing a Get in the workspace
  • Pending changes or undoing pending changes in the workspace
  • Shelving or checking in from the workspace
  • Resolving conflicts in the workspace

We lifted both of these limitations in TFS 2010. Adding the ability to change the owner of a workspace was not difficult, but allowing workspaces to be used by more than just their owner required us to make workspaces fully securable objects. We leveraged the new security service to make these changes and settled on four permissions that a user can have for a workspace. Those permissions are: Read, Use, CheckIn, and Administer.

  • The Use permission covers operations that change the state of a workspace: its definition (mappings), local versions, conflicts, or pending changes. Most operations that you use on a day-to-day basis are checking for the Use permission on a workspace. See the list at the top of this post for some examples.

  • We separated out the ability to check in into its own permission, CheckIn. The reasoning was that there may be some scenarios for shared workspaces where only certain users (like the owner) would be allowed to check in.

  • A user needs the Administer permission to change the mappings, name, owner, comment, or computer of a workspace. They also need this permission to delete the workspace from the system or make changes to the access control list for the workspace.

  • The Read permission exists as a permission but is not enforced. It would theoretically cover the ability to see that a workspace exists, to view the mappings for a workspace, and to view the pending changes for a workspace. In TFS 2005 and 2008, any valid user could view these properties of any other workspace, and for 2010 we have not altered this behavior.

Changing workspace permissions with the Edit Workspace dialog

TFS 2010 does not ship with a full UI for manipulating workspace permissions. Users are limited to choosing between three "permission profiles" for their workspaces; a permission profile is essentially a template for the workspace's access control list. The default permission profile is "Private workspace". A private workspace has the same effective behavior as a workspace in TFS 2005 and 2008: the workspace can only be used by its owner. The access control list for a private workspace has only one entry, granting all permissions to the owner -- for example, John Smith:

YOURDOMAIN\johnsmith: Read, Use, CheckIn, Administer

privateworkspace

The two other permission profiles we ship are "Public-limited" and "Public". These two profiles continue to grant all permissions to the owner. They additionally grant any valid user additional permissions. For public-limited, other users are granted the Read and Use permissions on the workspace. For a fully public workspace, every valid user of the team project collection has the same permissions as the owner: Read, Use, CheckIn, Administer.

publicworkspace

Using a public workspace from Visual Studio

 

You'll need to log onto the machine which has the public workspace. After starting Visual Studio 2010 and connecting to the server which has a public workspace, you'll be able to see the workspace in the appropriate drop-down combo boxes in the Source Control Explorer and Pending Changes toolwindows.

workspacedropdown_sceUsers of Visual Studio 2008 and earlier will not be able to see public workspaces belonging to other users. They will continue to see only their own workspaces.

workspacedropdown_2008In this example, I made the shared workspace "Public-limited." You'll see that because I lack the Administer permission for this workspace, I can only view the workspace's mappings, owner, comment, and permissions profile. The controls are read-only.

edit_publiclimited

Using a public workspace from the command line (tf.exe)

Again, you'll need to log onto the machine which has the public workspace. Once you've started a Visual Studio 2010 command prompt, cd to a mapped path for the workspace. In my case, this is D:\Proj. You can see below that I tried to run a command, but it failed to determine the workspace. This is because the local workspace cache file is per-user and this user has never heard of the workspace in question. I can either:

  1. Start Visual Studio once and connect to the team project collection. This will populate the local workspace cache.
  2. Run “tf workspaces” to manually populate the cache file, as the command suggests.

I chose to run “tf workspaces”. We can see that the workspace belonging to the other user is displayed, confirming that I have access to the public workspace. Now my "tf get" command succeeds.

commandline Artifact ownership

If user B shelves changes out of user A's workspace, user B is the owner of the shelveset that was created. Likewise, if user B checks in changes from user A's workspace, user B is recorded as the user who committed the changes.

Individual pending changes do not have owners -- only workspaces have owners. If user A checks out file.txt for edit in user B's workspace, some UI components may state that "file.txt is opened for edit by user B," even though user A is the one who pended the change. It would be more accurate to state that "file.txt is opened for edit in workspace X, which is owned by user B."

Failed permission checks

The TFS 2005/2008 error message "TF14091: You cannot perform this operation on workspace {0} because you are not the owner of the workspace." is no longer issued by TFS 2010. Instead, you will receive the following message indicating which workspace permission the operation demanded:

TF204017: The operation cannot be completed because the user ({0}) does not have one or more required permissions ({1}) for workspace {2}.

In the example below, the workspace was switched back to "Private" just before the command was issued.

failed_perm_check AdminWorkspaces global privilege

All versions of TFS from 2005 through 2010 have a global privilege called "Administer Workspaces," or AdminWorkspaces for short. By default this privilege is granted to team project collection administrators. Users possessing the AdminWorkspaces privilege automatically get the Administer permission on all workspaces in the system, even if they ordinarily would not receive it. This enables administrators to purge old workspaces in the system, and take ownership of workspaces belonging to employees on vacation, or contractors who are no longer with the team. Users with the AdminWorkspaces privilege can also create workspaces on behalf of other users.

adminworkspaces_gui

If I am granted the AdminWorkspaces privilege and go back to the earlier example, where I was locked out of the Edit Workspace dialog because the workspace was "Public-limited", we can see that I now have full access. My effective permissions are "Read, Use, Administer". If I want the CheckIn permission, I could grant it to myself by changing the permission profile of the workspace to "Public".

edit_publiclimited_adminworkspaces

Custom permissions

While the UI restricts you to the three in-box permission templates, the server has full support for custom access control lists on workspaces. You can query the full access control list by using the security service and the version control service together. The GUID of the security namespace for workspaces is a well-known constant, and the version control client object model has the token in that namespace for any given workspace, accessible through the Workspace.SecurityToken property. The following code sample shows how to extract and display the access control list for the workspace inferred by the current directory.

(This code sample needs assembly references to MS.TeamFoundation.Common, MS.TeamFoundation.Client, MS.TeamFoundation.VersionControl.Common, and MS.TeamFoundation.VersionControl.Client.)

 using Microsoft.TeamFoundation;

using Microsoft.TeamFoundation.Common;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Framework.Common;
using Microsoft.TeamFoundation.Framework.Client;
using Microsoft.TeamFoundation.VersionControl.Common;
using Microsoft.TeamFoundation.VersionControl.Client;

 // Use the current directory to infer the workspace and TFS team project collection.
WorkspaceInfo wi = Workstation.Current.GetLocalWorkspaceInfo(Environment.CurrentDirectory);
TfsTeamProjectCollection tpc = new TfsTeamProjectCollection(wi.ServerUri);
VersionControlServer vcs = tpc.GetService<VersionControlServer>();
Workspace workspace = vcs.GetWorkspace(wi);

// Get the identity service for the TPC.
IIdentityManagementService ims = tpc.GetService<IIdentityManagementService>();

// Get the security service for the TPC and use it to get the workspace security namespace.
ISecurityService security = tpc.GetService<ISecurityService>();
SecurityNamespace workspaceSecurityNamespace = 
                       security.GetSecurityNamespace(SecurityConstants.WorkspaceSecurityNamespaceGuid);

// Get the access control list for the workspace, using the workspace's security token in the namespace.
AccessControlList acl = workspaceSecurityNamespace.QueryAccessControlList(workspace.SecurityToken,
                        null, false);

// Get the full TeamFoundationIdentity objects for the IdentityDescriptor of each access control entry.
List<IdentityDescriptor> descriptors = new List<IdentityDescriptor>();

foreach (AccessControlEntry ace in acl.AccessControlEntries)
    descriptors.Add(ace.Descriptor);

TeamFoundationIdentity[] identities = ims.ReadIdentities(descriptors.ToArray(), MembershipQuery.None,
                          ReadIdentityOptions.None);

// Display the access control list to the user.
Console.WriteLine("Access control list for " + workspace.DisplayName + Environment.NewLine);

int i = 0;

foreach (AccessControlEntry ace in acl.AccessControlEntries)
{
    if (null == identities[i])
        Console.WriteLine("  " + ace.Descriptor.Identifier + ":");
    else
        Console.WriteLine("  " + IdentityHelper.GetDomainUserName(identities[i]) + ":");

    if (0 != ace.Allow)
        Console.WriteLine("     Allow: " + ((WorkspacePermissions)ace.Allow).ToString());

    if (0 != ace.Deny)
        Console.WriteLine("     Deny: " + ((WorkspacePermissions)ace.Deny).ToString());

    i++;
}

Going back to our public-limited workspace example, we can now use this code to visualize the full access control list for the workspace.

readacl

Modifying the access control list

We can take the above code fragment one step further and manipulate the access control list. Let’s remove the access control entry for [Collection0]\Project Collection Valid Users and replace it with an entry that explicitly gives full permissions only to REDMOND\vseqa1. Performing this operation requires Administer permission on the workspace, either explicitly or through the AdminWorkspaces global privilege.

 // Use the current directory to infer the workspace and TFS team project collection.
WorkspaceInfo wi = Workstation.Current.GetLocalWorkspaceInfo(Environment.CurrentDirectory);
TfsTeamProjectCollection tpc = new TfsTeamProjectCollection(wi.ServerUri);
VersionControlServer vcs = tpc.GetService<VersionControlServer>();
Workspace workspace = vcs.GetWorkspace(wi);

// Get the security service for the TPC and use it to get the workspace security namespace.
ISecurityService security = tpc.GetService<ISecurityService>();
SecurityNamespace workspaceSecurityNamespace = 
        security.GetSecurityNamespace(SecurityConstants.WorkspaceSecurityNamespaceGuid);

// Get the access control list for the workspace, using the workspace's security token in the namespace.
AccessControlList acl = workspaceSecurityNamespace.QueryAccessControlList(workspace.SecurityToken,
                           null, false);

WorkspacePermissions allWorkspacePermissions = WorkspacePermissions.Read | WorkspacePermissions.Use |
                                          WorkspacePermissions.CheckIn | WorkspacePermissions.Administer;

acl.RemoveAccessControlEntry(new IdentityDescriptor(IdentityConstants.TeamFoundationType,
                      GroupWellKnownSidConstants.EveryoneGroupSid));
acl.SetAccessControlEntry(new AccessControlEntry(tpc.AuthorizedIdentity.Descriptor,
                                 (int)allWorkspacePermissions, 0), false);

workspaceSecurityNamespace.SetAccessControlList(acl);

After running this code I can see that the workspace access control list now has two entries – one for the workspace owner and one for me (REDMOND\vseqa1).

readacl_afterchangeBecause the workspace’s access control list no longer matches one of the predefined permission profiles (templates), the permissions profile in the Edit Workspace is shown as “Custom permissions”.

custompermissionsIf you leave the workspace permissions combo box set to “Custom permissions,” then you can change any other property of the workspace without overwriting your customized access control list. But if you later want to go back to one of the pre-defined permission profiles, just select it from the combo box and hit OK. Your custom access control list will be wiped and replaced.

What happens if I accidentally delete the ACE for myself (the owner)?

Version control permission checks will still succeed, even if the owner ACE does not exist in the security service’s access control list. The version control service still knows that you are the owner of the workspace and therefore have full permissions. You will be locked out of making changes to the access control list through the security service until the owner ACE is restored.

To restore the owner ACE, open the Edit Workspace dialog and make any change (change the comment, for example). When you click OK and the workspace is updated on the server, the owner ACE will be restored.