Segregation of Duties

Feb 3, 2010 at 6:00 PM

I've been working on implementing TFS Deployer into our process. So far it seems to be really easy and straightforward. There is one part where I am stuck though.

We have 4 different environments, and only certain people are allowed to push to each environment:

  • DEV - Lead developer
  • Integration - Lead QA
  • Beta/Prod - Release Manager

TFS only gives you the ability allow or deny people the ability to change the build quality, but not restrict who can deploy to which environments.

One thing we would like to be able to do is to give the Development team the ability to deploy to their own environment, while keeping them from deploying to the others.

I think this can be accomplished by adding a field to the mappings file: AuthenticatedUsers. which takes an NT login or group.

If the person who triggered the change (triggerEvent.ChangedBy) is in the mapping file, then it executes the script.

Feb 3, 2010 at 6:02 PM
Edited Feb 3, 2010 at 6:08 PM

A little more investigation shows a "PermittedUsers" field.

I'll play with that and see if it will let me do what I need.

Feb 3, 2010 at 6:22 PM

I've actually extended the mappings file to check a named TFS Security Group and named TFS Users.  This is what I think you are looking for.

Feb 3, 2010 at 6:27 PM

That does sound like what I'm looking for.

Right now it looks like I can specify each user individually (seperated by a ";") but that's a huge hassle to deal with if people are added or removed. I'd really like to use the groups.

Will the group membership work with the current codebase, or is that something you tweaked on your own?

Feb 3, 2010 at 6:48 PM

It was an extension to the current codeplex code.  (I should probably submit this change.) This was part of a much larger set of changes. for a customer but it is very easy to implement.  You to make several small changes to several differrent files: XSD, Evaluator, etc.  If you follow the pattern for PermittedUsers it is very similar:

 One note on the PermittedGroup.  you should probably add a Regex validater for the Name list.  I did used a comma and got a nice runtime error which I hadn't trapped.  The console mode is very useful.

Here is the code for my MappingEvaluator.cs.  This also allows for a ComputerName=<wildcard>.

using System;
using System.Net;
using Readify.Useful.TeamFoundation.Common;
using Readify.Useful.TeamFoundation.Common.Notification;
using Microsoft.TeamFoundation.Server;
using TfsDeployer.Properties;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
using Microsoft.TeamFoundation.Build.Client;
using System.Diagnostics;

namespace TfsDeployer
{
    public class MappingEvaluator : IMappingEvaluator
    {
        public bool DoesMappingApply(object config, object Source)
        {
            string wildcardQuality = Properties.Settings.Default.BuildQualityWildcard;
            bool isComputerMatch = false;
            bool isSystemAccount = false;
            bool isOldValueMatch = false;
            bool isNewValueMatch = false;
            bool isDifferent = false;
            bool isUserPermitted = false;
            bool isGroupPermitted = false;
            Mapping mapping = (Mapping)config;
            BuildStatusChangeEvent triggerEvent = (BuildStatusChangeEvent)Source;
            var statusChange = triggerEvent.StatusChange;

            try
            {

                isComputerMatch = IsComputerMatch(mapping.Computer);

                isSystemAccount = TfsDeployerHelper.IsSystemAccount();
                isOldValueMatch = IsQualityMatch(statusChange.OldValue, mapping.OriginalQuality, wildcardQuality);
                isNewValueMatch = IsQualityMatch(statusChange.NewValue, mapping.NewQuality, wildcardQuality);
                isDifferent = (mapping.OriginalQuality == wildcardQuality) && (String.Compare(statusChange.NewValue, mapping.NewQuality, true) != 0);
                isUserPermitted = IsUserPermitted(triggerEvent, mapping);
                isGroupPermitted = IsGroupPermitted(triggerEvent, mapping);

                TraceHelper.TraceInformation(TraceSwitches.TfsDeployer,
                                  "Mapping evaluation details:\n" +
                                  "    MachineName={0}, MappingComputer={1}\n" +
                                  "    BuildOldStatus={2}, BuildNewStatus={3}\n" +
                                  "    MappingOrigQuality={4}, MappingNewQuality={5}\n" +
                                  "    UserIsPermitted={6}, EventCausedBy={7}",
                    Environment.MachineName, mapping.Computer, statusChange.OldValue, statusChange.NewValue, mapping.OriginalQuality, mapping.NewQuality, isUserPermitted, triggerEvent.ChangedBy);

                TraceHelper.TraceInformation(TraceSwitches.TfsDeployer,
                                  "Eval results:\n" +
                                  "    isComputerMatch={0}, isOldValueMatch={1}, isNewValueMatch={2}, isUserPermitted={3}, isGroupPermitted={4}, isDifferent={5}",
                                  isComputerMatch, isOldValueMatch, isNewValueMatch, isUserPermitted, isGroupPermitted, isDifferent);
            }
            catch (Exception ex)
            {
                TraceHelper.TraceError(TraceSwitches.TfsDeployer, ex);
                EventLog.WriteEntry("TfsDeployer", "MappingEvaluator::DoesMappingApply::" + ex.ToString(), EventLogEntryType.Error);
            }

            return isComputerMatch && isOldValueMatch && isNewValueMatch && isUserPermitted && isGroupPermitted;
        }

        private static bool IsComputerMatch(string mappingComputerName)
        {
            if (mappingComputerName == Settings.Default.BuildQualityWildcard || mappingComputerName == "" || mappingComputerName == String.Empty) return true;
            var hostNameOnly = Dns.GetHostName().Split('.')[0];
            return string.Equals(hostNameOnly, mappingComputerName, StringComparison.InvariantCultureIgnoreCase);
        }

        private static bool IsQualityMatch(string eventQuality, string mappingQuality, string wildcardQuality)
        {
            eventQuality = eventQuality ?? string.Empty;
            mappingQuality = mappingQuality ?? string.Empty;
            if (string.Compare(mappingQuality, wildcardQuality, true) == 0) return true;
            return string.Compare(mappingQuality, eventQuality, true) == 0;
        }

        private static bool IsUserPermitted(BuildStatusChangeEvent changeEvent, Mapping mapping)
        {
            if (mapping.PermittedUsers == null) return true;

            var permittedUsers = mapping.PermittedUsers.Split(';');
            foreach (var userName in permittedUsers)
            {
                if (string.Equals(changeEvent.ChangedBy, userName, StringComparison.CurrentCultureIgnoreCase))
                {
                    return true;
                }
            }

            return false;
        }

        private static bool IsGroupPermitted(BuildStatusChangeEvent changeEvent, Mapping mapping)
        {
            if (mapping.PermittedGroups == null) return true;

            var permittedGroups = mapping.PermittedGroups.Split(';');
            IGroupSecurityService gss = ServiceHelper.GetService<IGroupSecurityService>();

            foreach (var groupName in permittedGroups)
            {
                if (groupName != null && groupName.Trim().Length > 0)
                {
                    Identity idSID = null;
                    Identity[] idUserNames = null;
                    try
                    {
                        idSID = gss.ReadIdentity(SearchFactor.AccountName, groupName, QueryMembership.Expanded);
                        idUserNames = gss.ReadIdentities(SearchFactor.Sid, idSID.Members, QueryMembership.None);
                    }
                    catch (Exception ex)
                    {
                        EventLog.WriteEntry("TfsDeployer", "MappingEvaluator::IsGroupPermitted::" + ex.ToString(), EventLogEntryType.Error);

                    }

                    foreach (Identity id in idUserNames)
                    {
                        if (string.Equals(changeEvent.ChangedBy, id.Domain + "\\" + id.AccountName, StringComparison.CurrentCultureIgnoreCase))
                        {
                            return true;
                        }
                    }
                }
            }

            return false;
        }

    }
}

 

 

Feb 3, 2010 at 7:57 PM

Thanks!

Looks like you made quite a few changes to the base classes (or I have a different version of the source code, I just downloaded the latest version).

This get's me on the right path though. I'll post back my solution once I'm finished.

Feb 3, 2010 at 8:00 PM

Yes.  I thought it would be simpler to just post the complete source then edit.  The IsGroupPermitted() method should be able to be used as is and at least contains the code for obtaining the Group Membership.

Feb 3, 2010 at 9:49 PM
Edited Feb 3, 2010 at 9:52 PM

I didn't have to do too much to get it wired in:

  • Add the IsGroupPermitted method
        private static bool IsGroupPermitted(BuildStatusChangeEvent changeEvent, Mapping mapping)
        {
            if (mapping.PermittedGroups == null) return true;

            var permittedGroups = mapping.PermittedGroups.Split(';');
            IGroupSecurityService gss = ServiceHelper.GetService<IGroupSecurityService>();

            foreach (var groupName in permittedGroups)
            {
                if (groupName != null && groupName.Trim().Length > 0)
                {
                    Identity idSID = null;
                    Identity[] idUserNames = null;
                    try
                    {
                        idSID = gss.ReadIdentity(SearchFactor.AccountName, groupName, QueryMembership.Expanded);
                        idUserNames = gss.ReadIdentities(SearchFactor.Sid, idSID.Members, QueryMembership.None);
                    }
                    catch (Exception ex)
                    {
                        TraceHelper.TraceError(TraceSwitches.TfsDeployer, ex);

                        //EventLog.WriteEntry("TfsDeployer", "MappingEvaluator::IsGroupPermitted::" + ex.ToString(), EventLogEntryType.Error);

                    }

                    foreach (Identity id in idUserNames)
                    {
                        if (string.Equals(changeEvent.ChangedBy, id.Domain + "\\" + id.AccountName, StringComparison.CurrentCultureIgnoreCase))
                        {
                            return true;
                        }
                    }
                }
            }

            return false;
        }

  • Modify the DeploymentMapping.cs file - Add permittedGroupsField and property.
    private string permittedGroupsField;

    /// <remarks/>
    [System.Xml.Serialization.XmlAttributeAttribute()]
    public string PermittedGroups
    {
        get
        {
            return this.permittedGroupsField;
        }
        set
        {
            this.permittedGroupsField = value;
        }
    }

  • Download and install the VisualStudio 2008 SDK -
    • Add the reference to the Microsoft.TeamFoundation assembly
    • Add the reference to the Microsoft.TeamFoundation.Common assembly.
  • Add the IsGroupPermitted check to the DoesMappingApply method
        public bool DoesMappingApply(Mapping mapping, BuildStatusChangeEvent triggerEvent, string buildStatus)
        {
            var statusChange = triggerEvent.StatusChange;

            bool isStatusUnchanged = string.Equals(statusChange.NewValue, statusChange.OldValue, StringComparison.InvariantCultureIgnoreCase);
            if (isStatusUnchanged) return false;

            bool isBuildStatusMatch = IsBuildStatusMatch(mapping, buildStatus);
            bool isComputerMatch = IsComputerMatch(mapping.Computer);

            string wildcardQuality = Properties.Settings.Default.BuildQualityWildcard;
            bool isOldValueMatch = IsQualityMatch(statusChange.OldValue, mapping.OriginalQuality, wildcardQuality);
            bool isNewValueMatch = IsQualityMatch(statusChange.NewValue, mapping.NewQuality, wildcardQuality);
            bool isUserPermitted = IsUserPermitted(triggerEvent, mapping);
            bool isGroupPermitted = IsGroupPermitted(triggerEvent, mapping);

            TraceHelper.TraceInformation(TraceSwitches.TfsDeployer,
                              "Mapping evaluation details:\n" +
                              "    MachineName={0}, MappingComputer={1}\n" +
                              "    BuildOldStatus={2}, BuildNewStatus={3}\n" +
                              "    MappingOrigQuality={4}, MappingNewQuality={5}\n" +
                              "    UserIsPermitted={6}, isGroupPermitted={7}\n" +
                              "    EventCausedBy={8}\n" +
                              "    BuildStatus={9}, MappingStatus={10}",
                Environment.MachineName, mapping.Computer, 
                statusChange.OldValue, statusChange.NewValue, 
                mapping.OriginalQuality, mapping.NewQuality,
                isUserPermitted, isGroupPermitted,
                triggerEvent.ChangedBy,
                buildStatus, mapping.Status);

            TraceHelper.TraceInformation(TraceSwitches.TfsDeployer,
                              "Eval results:\n" +
                              "    isComputerMatch={0}, isOldValueMatch={1}, isNewValueMatch={2}, isUserPermitted={3}, isGroupPermitted={4}, isBuildStatusMatch={5}",
                              isComputerMatch, isOldValueMatch, isNewValueMatch, isUserPermitted, isGroupPermitted, isBuildStatusMatch);

            return isComputerMatch && isOldValueMatch && isNewValueMatch && isUserPermitted && isGroupPermitted && isBuildStatusMatch;
            
        }

  • Update the TfsDeployer/Configuration/DeploymentMappings.xsd to include the attribute for PermittedGroups
            <xs:attribute name="PermittedGroups" type="xs:string" form="unqualified" />

  • Lastly, update the mapping file to include the group name I wanted to have access:
    <Mapping xmlns=""
         Computer="ComputerNameHere"
         OriginalQuality="Unexamined"
         NewQuality="Rejected"
         Script="DoSomething.ps1"
         PermittedGroups="[TFSProject]\Project Administrators;"
         NotificationAddress="YourEmail@YourCompany.com">
    </Mapping>
Thanks a ton for your help. Opening up the source code has me thinking of a few other things I'd like to change, like the email outputs, etc.
Now I just have to go through and create the groups in TFS and update all of my deployment mapping files.
Feb 3, 2010 at 10:08 PM

That's it.  Simple.  I added a lot of additional fields and functions such as "ExitLoop"  so that "downstream" states are ignored.  Also I added several additional parameters to pass to the powershell scripts.

In addition I created an extensive TFS cmdlet snap-in for the Powershell scripts to use.