Esempio n. 1
0
        public Collection<PSObject> RunPowerShell(Dictionary<string, object> arguments, LoginViewModel clientId)
        {
            // Start with identity assigned by IIS Application Pool
            var poolIdentity = System.Security.Principal.WindowsIdentity.GetCurrent();

            // TODO: remove, this is for initial testing
            //scriptPath = Path.Combine(DT2.Properties.Settings.Default.PowerShellScriptsFolder, "TestScript.ps1");
            Command command = new Command(scriptPath);

            if (arguments != null)
            {
                foreach (var argument in arguments)
                {
                    command.Parameters.Add(argument.Key, argument.Value);
                }
            }
            if (debug)
            {
                command.Parameters.Add("debug");
            }
            command.Parameters.Add("log", logger);

            // add the ndc context
            string ndcContext = log4net.NDC.Pop();
            log4net.NDC.Push(ndcContext);
            command.Parameters.Add("ndcContext", ndcContext);

            try
            {
                logger.Debug("Application Pool identity is " + poolIdentity.Name + ", but we will impersonate " +
                             clientId.UserName);

                if (System.Security.SecurityContext.IsWindowsIdentityFlowSuppressed())
                {
                    logger.Error("PowerShell calls will fail, because IsWindowsIdentityFlowSuppressed true");
                }

                // A RunSpace defines the operating system environment, which references HKCU\Environment
                // We create it now before impersonating an identity that might not have HKCU\Environment access
                // TODO: it may be possible to improve performance by using a RunSpacePool
                using (PowerShell powerShell = PowerShell.Create())
                {
                    Runspace runspace = RunspaceFactory.CreateRunspace();
                    runspace.Open();
                    powerShell.Runspace = runspace;
                    Collection<PSObject> results;

                    IntPtr handle;
                    SafeTokenHandle _handle;
                    /// Test:  generate primary login token
                    /// LOGON32_PROVIDER_DEFAULT = 0
                    /// 
                    logger.Debug("LogonUser call for " + clientId.UserNameNoDomain);
                    bool logonSuccess = NativeMethods.LogonUser(clientId.UserNameNoDomain, clientId.DomainName,
                        clientId.Password,
                        (int) LogonType.Interactive, 0, out handle);
                    _handle = new SafeTokenHandle(handle);

                    if (!logonSuccess)
                    {
                        string errMsg = "LogonUser() for " + clientId.UserName +
                                        "failed - no handle for user credentials:" +
                                        Marshal.GetLastWin32Error();
                        throw new Win32Exception(errMsg);
                    }


                    // When 'using' block ends, the thread reverts back to previous Windows identity,
                    // because under the hood WindowsImpersonationContext.Undo() is called by Dispose()
                    try
                    {
                        using (
                            WindowsImpersonationContext wic = WindowsIdentity.Impersonate(_handle.DangerousGetHandle()))
                        {
                            // WindowsIdentity will have changed to match clientId
                            var clientIdentity = System.Security.Principal.WindowsIdentity.GetCurrent();
                            logger.Debug("Application Pool identity is now " + clientIdentity.Name);

                            powerShell.Commands.AddCommand(command);
                            logger.Debug("Calling " + scriptPath);
                            results = powerShell.Invoke();
                        }
                    }
                    // Back to the original identity
                    finally
                    {
                        // dispose of the LogonUser handle
                        _handle.Dispose();
                        // clean up RunSpace used by the PowerShell object
                        runspace.Close();
                        runspace.Dispose();
                    }

                    // The order is important here. Debug messages are flushed to the log *before* checking for errors
                    // so the debug traces leading up to an error are not lost 
                    Collection<PSObject> filteredResults = new Collection<PSObject>();
                    foreach (PSObject result in results)
                    {
                        string output = result.BaseObject as string;
                        if ((output != null) && output.StartsWith(DebugPrefix))
                        {
                            if (debug)
                            {
                                logger.Info(output.Substring(DebugPrefix.Length));
                            }
                        }
                        else
                        {
                            filteredResults.Add(result);
                        }
                    }
                    foreach (DebugRecord r in powerShell.Streams.Debug)
                    {
                        logger.Info(r.Message);
                    }
                    logger.Debug("Examining powershell error records");
                    foreach (ErrorRecord r in powerShell.Streams.Error)
                    {
                        // If the exception doesn't match a "to be ignored" exception, then throw it
                        if (IgnoreExceptions.SingleOrDefault(i =>
                            i.Equals(r.Exception.GetType().FullName, StringComparison.InvariantCultureIgnoreCase)) ==
                            null)
                        {
                            logger.Error("Powershell reported exception:" + r.ErrorDetails);
                            throw r.Exception;
                        }
                    }
                    return filteredResults;
                }
            }
            catch (Exception e)
            {
                logger.Error(e);
                logger.Error(e.Message);
                logger.Error(e.StackTrace);
                throw;
            }
        }
 public static string JsonSerialize(LoginViewModel that)
 {
     return Json.Encode(that);
 }
Esempio n. 3
0
 private static void RunLongScript(PsWrapper script, Dictionary <string, object> psargs, LoginViewModel clientId,
                                   string ndcContext,
                                   Action callAction = null)
 {
     using (log4net.NDC.Push(ndcContext))
     {
         try
         {
             script.RunPowerShell(psargs, clientId);
             logger.Info("Completed RunPowerShell ");
             if (callAction != null)
             {
                 logger.Info("Completed RunPowerShell, and calling callAction");
                 callAction();
             }
         }
         catch (Exception e)
         {
             var errMsg = e.Message;
             logger.Error("In RunLongScript we saw exception with message " + errMsg);
         }
     }
 }
Esempio n. 4
0
        public static List <XenDesktopInventoryItem> GetActiveDirectoryUsers(string query)
        {
            var users      = new List <XenDesktopInventoryItem>();
            var grps       = new List <XenDesktopInventoryItem>();
            var usersFinal = new List <XenDesktopInventoryItem>();

            LoginViewModel clientId =
                LoginViewModel.JsonDeserialize(((FormsIdentity)HttpContext.Current.User.Identity).Ticket);

            logger.Debug("Find users and groups in domain of " + clientId.UserName);

            string searchDomain = clientId.DomainName;

            try
            {
                PrincipalContext adController = new PrincipalContext(ContextType.Domain, searchDomain, clientId.UserName, clientId.Password);

                UserPrincipal user = new UserPrincipal(adController);
                user.SamAccountName = query;
                PrincipalSearcher usrSrch = new PrincipalSearcher(user);
                PrincipalSearchResult <Principal> userSearchResult = usrSrch.FindAll();
                logger.Debug("Search for users netted " + userSearchResult.Count() + " results.");

                GroupPrincipal grp = new GroupPrincipal(adController);
                grp.SamAccountName = query;
                PrincipalSearcher grpSrch = new PrincipalSearcher(grp);
                PrincipalSearchResult <Principal> grpSearchResult = grpSrch.FindAll();
                logger.Debug("Search for groups netted " + grpSearchResult.Count() + " results.");

                int grpCount = 20;
                foreach (var currGrp in grpSearchResult)
                {
                    if (UsersSkipList.Contains(currGrp.SamAccountName))
                    {
                        continue;
                    }

                    var userRsrc = new XenDesktopInventoryItem
                    {
                        Id   = searchDomain + "\\" + currGrp.SamAccountName,
                        Name = searchDomain + "\\" + currGrp.SamAccountName + "(group)"
                    };

                    grps.Add(userRsrc);

                    grpCount--;
                    if (grpCount < 0)
                    {
                        break;
                    }
                }
                grps.Sort(new Comparison <XenDesktopInventoryItem>(XenDesktopInventoryItem.Compare));

                int userCount = 100;
                foreach (var currUser in userSearchResult)
                {
                    if (UsersSkipList.Contains(currUser.SamAccountName))
                    {
                        continue;
                    }

                    var userRsrc = new XenDesktopInventoryItem
                    {
                        Id   = searchDomain + "\\" + currUser.SamAccountName,
                        Name = searchDomain + "\\" + currUser.SamAccountName
                    };

                    users.Add(userRsrc);

                    userCount--;
                    if (userCount < 0)
                    {
                        break;
                    }
                }

                users.Sort(new Comparison <XenDesktopInventoryItem>(XenDesktopInventoryItem.Compare));
                usersFinal = grps.Concat(users).ToList();
            }
            catch (System.Exception ex)
            {
                var errmsg = "Problem searching for users in " + searchDomain + "  Details: " + ex.Message;
                logger.Error(errmsg);
                logger.Error(ex);
            }
            return(usersFinal);
        }
Esempio n. 5
0
        /// <summary>
        /// The list of users associated with a DesktopGroup is in the DesktopGroupAccessPolicy.  In our case,
        /// there are two per desktop group.  One is used when accessing the group through an AG (Access Gateway).
        /// The other is used when accessing the desktop directly.
        /// </summary>
        /// <param name="catalogName"></param>
        /// <param name="results"></param>
        private static void GetBrokerCatalogUsers(string catalogName, List <Catalog> result)
        {
            string desktopGrpAccessPolicyName = string.IsNullOrEmpty(catalogName)
                ? null
                : catalogName + ScriptNames.DesktopGroupSuffix + ScriptNames.AccessPolicySuffixForDirect;
            Dictionary <string, object> psargs = new Dictionary <string, object>();

            psargs.Add("desktopGroupPolicyName", desktopGrpAccessPolicyName);

            var listProvScript =
                new PsWrapper(Path.Combine(DT2.Properties.Settings.Default.PowerShellScriptsFolder,
                                           ScriptNames.GetDesktopGroupsAccessPolicy));
            LoginViewModel clientId =
                LoginViewModel.JsonDeserialize(((FormsIdentity)HttpContext.Current.User.Identity).Ticket);
            var psProvs = listProvScript.RunPowerShell(psargs, clientId);

            // TODO:  add the users to the list.
            foreach (PSObject item in psProvs)
            {
                dynamic includedUsers = item.Members["IncludedUsers"].Value;
                dynamic excludedUsers = item.Members["ExcludedUsers"].Value;
                string  name          = (string)item.Members["Name"].Value;

                // assertions
                if (includedUsers.Length < 1)
                {
                    var errMsg = "DesktopGroup for broker catalog " + catalogName + " has no included users!";
                    logger.Error(errMsg);
                }
                if (excludedUsers.Length > 0)
                {
                    var errMsg = "DesktopGroup for broker catalog " + catalogName + " *has* excluded users!";
                    logger.Error(errMsg);
                }

                List <string> userList = new List <string>();
                foreach (dynamic user in includedUsers)
                {
                    userList.Add(user.Name);
                }

                // Update corresponding Catalog
                int assertUsage = 0;
                foreach (var cat in result)
                {
                    if (catalogName + ScriptNames.DesktopGroupSuffix + ScriptNames.AccessPolicySuffixForDirect == name)
                    {
                        assertUsage++;
                        cat.Users = userList.ToArray();
                    }
                }

                // Make sure there is a value for 'Users'
                foreach (var cat in result)
                {
                    if (cat.Users == null)
                    {
                        cat.Users = new string[0];
                        var infoMsg = "DesktopGroup for broker catalog " + catalogName + " has NO users!";
                        logger.Error(infoMsg);
                    }
                }

                // Assert
                if (assertUsage > 1)
                {
                    var errMsg = "Provisioning scheme used in multiple catalogs: name " + name;
                    logger.Error(errMsg);
                }
            } // End foreach.
        }
Esempio n. 6
0
        public static void CreateCatalog(Catalog newCat)
        {
            try
            {
                var jsonEquiv = Newtonsoft.Json.JsonConvert.SerializeObject(newCat);
                logger.Info("Creating Catalog corresponding to " + Newtonsoft.Json.JsonConvert.ToString(jsonEquiv));
                Dictionary <string, object> psargs = new Dictionary <string, object>();
                psargs.Add("ddcAddress", DT2.Properties.Settings.Default.XenDesktopAdminAddress);
                psargs.Add("catalogName", newCat.Name);
                psargs.Add("catalogDesc", newCat.Description);
                psargs.Add("catalogSessionSupport", GetXenDesktopDesktopSessionSupport(newCat.DesktopType));
                psargs.Add("desktopAllocationType", GetXenDesktopDesktopAllocationType(newCat.DesktopType));
                psargs.Add("persistUserChanges", GetXenDesktopDesktopPersistUserChanges(newCat.DesktopType));
                psargs.Add("desktopCleanOnBoot", GetXenDesktopDesktopCleanOnBoot(newCat.DesktopType));
                psargs.Add("desktopDomain", DT2.Properties.Settings.Default.XenDesktopDomain);
                psargs.Add("templatePath", newCat.Template);
                psargs.Add("networkPath", newCat.Network);
                psargs.Add("hostingUnitName", DT2.Properties.Settings.Default.XenDesktopHostingUnitName);
                psargs.Add("controllerAddress", DT2.Properties.Settings.Default.XenDesktopDDC);
                logger.Info("Catalog has " + newCat.Users.Length + " AD accounts and  " + newCat.Count + " machines.");
                psargs.Add("desktopCount", newCat.Count);
                psargs.Add("computerOffering", newCat.ComputeOffering);

                // Naming scheme: Remove spaces and other special characters from the pool name, truncate at 12 characters
                string desktopNamingScheme = newCat.Name.Replace(" ", string.Empty);
                desktopNamingScheme = desktopNamingScheme.Length > 12
                    ? desktopNamingScheme.Substring(0, 12)
                    : desktopNamingScheme;
                desktopNamingScheme += "###";
                psargs.Add("desktopNamingScheme", desktopNamingScheme);

                // Parameters required to setup the corresponding desktopgroup
                psargs.Add("userNames", newCat.Users);
                psargs.Add("desktopGrpName", newCat.Name + ScriptNames.DesktopGroupSuffix);
                psargs.Add("machineCount", newCat.Count);

                string secGrps = DT2.Properties.Settings.Default.SecurityGroups;
                secGrps = secGrps ?? String.Empty;
                if (!string.IsNullOrEmpty((newCat.SecurityGroup)))
                {
                    secGrps = newCat.SecurityGroup;
                }
                psargs.Add("securityGroups", secGrps);

                var poshScript =
                    new PsWrapper(Path.Combine(DT2.Properties.Settings.Default.PowerShellScriptsFolder,
                                               ScriptNames.CreateDesktopGroupScript));
                var jsonEquiv2 = Newtonsoft.Json.JsonConvert.SerializeObject(psargs);
                logger.Info("Calling " + ScriptNames.CreateDesktopGroupScript + " with args: " + jsonEquiv2);

                var exToIgnore = "Citrix.Broker.Admin.SDK.SdkOperationException";
                poshScript.IgnoreExceptions.Add(exToIgnore);
                logger.Info("Ignoring exceptions of type " + exToIgnore +
                            " because the SDK generates them for Get-BrokerUser calls as part of normal operation.");


                // Assert
                if (newCat.DesktopType == PublishedDesktops)
                {
                    if ("Random" != (string)psargs["desktopAllocationType"])
                    {
                        var errMsg = "Wrong desktopAllocationType for " + newCat.Name + " of type " + newCat.DesktopType;
                        throw new ArgumentException(errMsg);
                    }
                    if ("MultiSession" != (string)psargs["catalogSessionSupport"])
                    {
                        var errMsg = "Wrong catalogSessionSupport for " + newCat.Name + " of type " + newCat.DesktopType;
                        throw new ArgumentException(errMsg);
                    }
                    if ("Discard" != (string)psargs["persistUserChanges"])
                    {
                        var errMsg = "Wrong persistUserChanges for " + newCat.Name + " of type " + newCat.DesktopType;
                        throw new ArgumentException(errMsg);
                    }
                    if (psargs["desktopCleanOnBoot"] == null)
                    {
                        var errMsg = "Wrong desktopCleanOnBoot  for " + newCat.Name + " of type " + newCat.DesktopType;
                        throw new ArgumentException(errMsg);
                    }
                }
                if (DT2.Properties.Settings.Default.TestDisableCatalogCreate)
                {
                    logger.Warn("Skipping Catalog create, as TestDisableCatalogCreate is set true");
                }
                else
                {
                    // Enable Windows Authentication in ASP.NET *and* IIS to ensure User.Identity is a FormsIdentity
                    LoginViewModel clientId =
                        LoginViewModel.JsonDeserialize(((FormsIdentity)HttpContext.Current.User.Identity).Ticket);
                    // add the ndc context
                    string ndcContext = log4net.NDC.Pop();
                    log4net.NDC.Push(ndcContext);

                    Task.Run(() => RunLongScript(poshScript, psargs, clientId, ndcContext, () =>
                    {
                        EmailAdmin(newCat, clientId);
                    }));
                }
            }
            catch (Exception e)
            {
                var errMsg = e.Message;
                logger.Error(errMsg);
            }
        }
Esempio n. 7
0
        private static void GetProvSchemeDetails(string catalogName, List <Catalog> result)
        {
            Dictionary <string, object> psargs = new Dictionary <string, object>();

            psargs.Add("catalogName", catalogName);

            var listProvScript =
                new PsWrapper(Path.Combine(DT2.Properties.Settings.Default.PowerShellScriptsFolder,
                                           ScriptNames.GetProvSchemesScript));

            LoginViewModel clientId =
                LoginViewModel.JsonDeserialize(((FormsIdentity)HttpContext.Current.User.Identity).Ticket);
            var psProvs = listProvScript.RunPowerShell(psargs, clientId);

            foreach (PSObject item in psProvs)
            {
                string   provName        = (string)item.Members["ProvisioningSchemeName"].Value;
                Guid     provId          = (Guid)item.Members["ProvisioningSchemeUid"].Value;
                string   templatePath    = (string)item.Members["MasterImageVM"].Value;
                string   serviceOffering = (string)item.Members["ServiceOffering"].Value;
                string[] securityGroups  = (item.Members["SecurityGroups"] == null
                    ? new string[0]
                    : (string[])item.Members["SecurityGroups"].Value);
                string securityGroup = securityGroups.Length > 0 ? securityGroups[0] : "";
                // Assert
                if (templatePath == null)
                {
                    var errMsg = "No template for provisioning scheme " + provName;
                    logger.Error(errMsg);
                    continue;
                }

                // Assert
                if (serviceOffering == null)
                {
                    var errMsg = "No Desktop compute offering for provisioning scheme " + provName;
                    logger.Error(errMsg);
                    continue;
                }

                string[] templateSplitPath = templatePath.Split(new char[] { '\\' });
                string   template          = templateSplitPath[templateSplitPath.Length - 1];

                // Assert
                if (!template.EndsWith(".template"))
                {
                    var errMsg = "Template ends with wrong extension, path is " + templatePath + " template name is " +
                                 template;
                    logger.Error(errMsg);
                    continue;
                }

                // Update corresponding Catalog
                int assertUsage = 0;
                foreach (var cat in result)
                {
                    if (cat.ProvisioningSchemeId == provId.ToString())
                    {
                        assertUsage++;
                        cat.Template        = template.Substring(0, template.Length - ".template".Length);
                        cat.ComputeOffering = serviceOffering.Substring(0,
                                                                        serviceOffering.Length - ".serviceoffering".Length);
                        cat.DiskSize = (int)item.Members["DiskSize"].Value;

                        // TODO: ask SDK team for alternative to dyanamic variables
                        dynamic networkList = item.Members["NetworkMaps"].Value;
                        dynamic netDetails  = networkList[0];
                        string  networkPath = (string)netDetails.NetworkPath;

                        string[] networkSplitPath = networkPath.Split(new char[] { '\\' });
                        string   networkName      = networkSplitPath[networkSplitPath.Length - 1];
                        cat.Network       = networkName.Substring(0, networkName.Length - ".network".Length);
                        cat.SecurityGroup = securityGroup;
                    }
                }

                // Assert
                if (assertUsage > 1)
                {
                    var errMsg = "Provisioning scheme used in multiple catalogs: name " + provName + " UUID " + provId;
                    logger.Error(errMsg);
                }
            } // End foreach.
        }
Esempio n. 8
0
        // TODO: update to search in domain of logged-in user
        public static String GetActiveDirectoryUserEmail(string domainQualifiedSAMAccountName, LoginViewModel clientId)
        {
            var    splitUserName  = domainQualifiedSAMAccountName.Split(new char[] { '\\' });
            var    samAccountName = splitUserName[splitUserName.Length - 1];
            String email          = null;

            try
            {
                logger.Debug("Find users and groups in domain of " + clientId.UserName);
                PrincipalContext adController = new PrincipalContext(ContextType.Domain, clientId.DomainName, clientId.UserName, clientId.Password);


                // Could be a user or a group:
                UserPrincipal user = new UserPrincipal(adController);
                user.SamAccountName = samAccountName;
                PrincipalSearcher usrSrch          = new PrincipalSearcher(user);
                Principal         userSearchResult = usrSrch.FindOne();

                if (userSearchResult == null)
                {
                    GroupPrincipal grp = new GroupPrincipal(adController);
                    grp.SamAccountName = samAccountName;
                    PrincipalSearcher grpSrch = new PrincipalSearcher(grp);
                    userSearchResult = grpSrch.FindOne();
                }

                if (userSearchResult == null)
                {
                    logger.Error("Could not find Principal in AD for " + domainQualifiedSAMAccountName);
                    return(email);
                }

                DirectoryEntry dirEntry = (DirectoryEntry)userSearchResult.GetUnderlyingObject();
                // TODO:  is this property guaranteed to be present, e.g. if I create a user called donall, will the email property still be present?
                email = dirEntry.Properties["mail"] == null ? null : (string)(dirEntry.Properties["mail"].Value);

                if (email == null)
                {
                    logger.Info("No email for user" + domainQualifiedSAMAccountName);
                }
            }
            catch (System.Exception ex)
            {
                var errmsg = "Problem searching for users. Details: " + ex.Message;
                logger.Error(errmsg);
            }
            return(email);
        }
Esempio n. 9
0
        // TODO: update to search in domain of logged-in user
        public static String GetActiveDirectoryUserEmail(string domainQualifiedSAMAccountName, LoginViewModel clientId)
        {
            var splitUserName = domainQualifiedSAMAccountName.Split(new char[] { '\\' });
            var samAccountName = splitUserName[splitUserName.Length - 1];
            String email = null;

            try
            {
                logger.Debug("Find users and groups in domain of " + clientId.UserName);
                PrincipalContext adController = new PrincipalContext(ContextType.Domain, clientId.DomainName, clientId.UserName, clientId.Password);


                // Could be a user or a group:
                UserPrincipal user = new UserPrincipal(adController);
                user.SamAccountName = samAccountName;
                PrincipalSearcher usrSrch = new PrincipalSearcher(user);
                Principal userSearchResult = usrSrch.FindOne();

                if (userSearchResult == null)
                {
                    GroupPrincipal grp = new GroupPrincipal(adController);
                    grp.SamAccountName = samAccountName;
                    PrincipalSearcher grpSrch = new PrincipalSearcher(grp);
                    userSearchResult = grpSrch.FindOne();
                }

                if (userSearchResult == null)
                {
                    logger.Error("Could not find Principal in AD for " + domainQualifiedSAMAccountName);
                    return email;
                }

                DirectoryEntry dirEntry = (DirectoryEntry)userSearchResult.GetUnderlyingObject();
                // TODO:  is this property guaranteed to be present, e.g. if I create a user called donall, will the email property still be present?
                email = dirEntry.Properties["mail"] == null ? null : (string)(dirEntry.Properties["mail"].Value);

                if (email == null)
                {
                    logger.Info("No email for user" + domainQualifiedSAMAccountName);
                }
            }
            catch (System.Exception ex)
            {
                var errmsg = "Problem searching for users. Details: " + ex.Message;
                logger.Error(errmsg);
            }
            return email;
        }
Esempio n. 10
0
 private static void RunLongScript(PsWrapper script, Dictionary<string, object> psargs, LoginViewModel clientId,
     string ndcContext,
     Action callAction = null)
 {
     using (log4net.NDC.Push(ndcContext))
     {
         try
         {
             script.RunPowerShell(psargs, clientId);
             logger.Info("Completed RunPowerShell ");
             if (callAction != null)
             {
                 logger.Info("Completed RunPowerShell, and calling callAction");
                 callAction();
             }
         }
         catch (Exception e)
         {
             var errMsg = e.Message;
             logger.Error("In RunLongScript we saw exception with message " + errMsg);
         }
     }
 }
Esempio n. 11
0
        public static void EmailAdmin(Catalog newCat, LoginViewModel clientId)
        {
            try
            {
                // Send an email to each of the users from the signed on admin
                var adminEmail = GetActiveDirectoryUserEmail(clientId.UserName, clientId);
                if (string.IsNullOrEmpty(adminEmail))
                {
                    logger.Error("No email for admin " + clientId.UserName);
                    return;
                }

                logger.Info("Email " + clientId.UserName + " to say that " + newCat.Name + " is done. ");
                var userAddr = new System.Net.Mail.MailAddress(adminEmail);
                var mailMsg = new System.Net.Mail.MailMessage();
                mailMsg.Subject = "Desktop Catalog " + newCat.Name + " is ready!";
                mailMsg.Body = "Finished creating " + newCat.Name + ".  You access it from " +
                               DT2.Properties.Settings.Default.XenDesktopStoreFrontUrl.ToString();
                mailMsg.From = userAddr;
                mailMsg.CC.Add(adminEmail);

                // Build list of users to send email to.
                foreach (var user in newCat.Users)
                {
                    var userEmail = GetActiveDirectoryUserEmail(user, clientId);

                    if (string.IsNullOrEmpty(adminEmail))
                    {
                        logger.Error("No email for desktop group user " + user);
                        continue;
                    }

                    logger.Debug("Alert to be sent to " + userEmail + " for " + user);
                    mailMsg.To.Add(userEmail);
                }
                logger.Debug("Sending alert for catalog " + newCat.Name);
                Utils.Utils.SendEmail(mailMsg);
            }
            catch (Exception e)
            {
                var errMsg = "Could not email desktop group creation alert";
                logger.Error(errMsg, e);
           }
        }