cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 

Security group provisioning - Remove on uninstall

Hello,

our customer is using App Portal 2021R2 and SCCM as deployment technology.
According to the administration guide, the security groups provisioning functions as follows:

 

secgroups.png

"In the event the program is uninstalled using SCCM App Portal, the user will be removed from the listed security groups."

Adding users to the security group works like a charm, but on choosing uninstall for this catalog object, it seems like App Portal just tries to add the user to the group again, which then fails because the user already is in this group:

secgroupprovisioning.png

We configured the catalog object to add to group on request / approval.
Is there something we are missing, like a specific config, or is our interpretation of this function just wrong?
Thanks in advance for your support, if you need more information, just let me know and I'll provide it.

Best regards,
Tim



(1) Solution
CharlesW
By Level 12 Flexeran
Level 12 Flexeran

This has been a limitation for some time (contrary to the documentation). All hope is not lost, however...  I might suggest creating a command action which calls the command Remove-ADGroupMember. As arguments, you would pass in the group name and the UserName. You could define the group name as a custom variable on your catalog item, and you could pass in ##UserName## as the user name argument.. The powershell script would likely look similar to the following:

param(
[string]$groupName, #specify a custom variable on the catalog item with the group name
[string]$userName #pass the variable ##UserName##
)
Remove-ADGroupMember -identity $groupName -Members $userName -Confirm:$false


You would add this command action to the "on success uninstall" event of your catalog item.

Let me know if you have any questions on the subject..

View solution in original post

(4) Replies
CharlesW
By Level 12 Flexeran
Level 12 Flexeran

This has been a limitation for some time (contrary to the documentation). All hope is not lost, however...  I might suggest creating a command action which calls the command Remove-ADGroupMember. As arguments, you would pass in the group name and the UserName. You could define the group name as a custom variable on your catalog item, and you could pass in ##UserName## as the user name argument.. The powershell script would likely look similar to the following:

param(
[string]$groupName, #specify a custom variable on the catalog item with the group name
[string]$userName #pass the variable ##UserName##
)
Remove-ADGroupMember -identity $groupName -Members $userName -Confirm:$false


You would add this command action to the "on success uninstall" event of your catalog item.

Let me know if you have any questions on the subject..

Another option is to use a simple SOAP web service to do the same as what Charlie suggested.  Within Flexera Services, we've created a SOAP web service with a number of helpful utility methods in it.  One of those is a method called AddRemoveADGroupMember.  I've include code from a recent version of this web service.  If you'd like to try it, just save the code as GCS_CustomWebService.asmx in the <App Portal>\Web\WS folder and then add the web service in the admin UI.

<%@ WebService Language="C#" Class="GCS_CustomWebService" %>

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Web;
using System.Web.Services;
using System.Data;
using System.Data.SqlClient;
using System.Net.Mail;
using System.Text;
using System.Threading;
using System.Configuration;
using System.IO;
using System.Web.Services.Protocols;
using AppPortal.Business;
using AppPortal.Business.Common;
using AppPortal.Business.FNMP;

/// <summary>
/// Author's Name:          Osagyefo Kouta-Lopatey
/// Author's Company:       Flexera Software
/// Author's Department:    Global Consulting Services
/// 
/// UPDATE LOG
/// DATE            NAME            DESCRIPTION
/// 06-SEP-2021     Jim Dempsey     Fixed GetTargetUserForRequest;
///                                 Fixed GetLoggedInUserPropertyValue and GetUserPropertyValue to return string instead of datatable
///                                 Updated to dynamically retrieve App Portal connection string;
///                                 Removed dependency on BLTest web service;
///                                 Removed customer-specific web methods.
/// 30-SEP-2021     Jim Dempsey     Added GetCatalogVariable and SetCatalogVariable web methods.
/// 19-JAN-2022     Jim Dempsey     Updated SetCatalogVariable to update catalog item UpdatedOn
/// 26-JAN-2022     Jim Dempsey     Added GCSLog and GCSErrorLog
/// 29-JAN-2022     Jim Dempsey     Added GetABSetting
/// 11-FEB-2022     Jim Dempsey     Added GetMachineDomainFromFNMS
///
/// </summary>

[WebService(Namespace = "http://flexerasoftware.com/AppPortal")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
// To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line. 
// [System.Web.Script.Services.ScriptService]
public class GCS_CustomWebService : System.Web.Services.WebService
{
    const string LogFile = "GCS_CustomWebService.log"; // The filename for the log file
    const string ErrorLogFile = "GCS_CustomWebService_Error.log"; // The filename for the error log file

    /// <summary>
    /// Get the value of a specific request variable
    /// </summary>
    /// <param name="requestID"></param>
    /// <param name="varName"></param>
    /// <returns></returns>
    [WebMethod(Description=@"Get the value of an App Portal request variable.")]
    public string GetVariableValue(int requestID, string varName)
    {
        //Log.l1("Getting value for " + varName + " variable...", LogFile);
        SelfService.ESD esd = new SelfService.ESD();
        DataTable dt = esd.getEmailVariables(requestID);
        dt.TableName = "EmailVariables";

        foreach (DataRow row in dt.Rows)
        {
            if (row.Field<string>(0) == "##" + varName + "##")
            {
                Log.l1("Variable Name: " + (row.Field<string>(0)) + "; Variable Value = " + (row.Field<string>(1)), LogFile, (int)LoggingLevel.NORMAL);
                return row.Field<string>(1);
            }
        }
        string result = string.Format("Could not retrieve value for request variable {0}", varName);
        Log.l1(result, LogFile, (int)LoggingLevel.NORMAL);
        return result;
    }

    /// <summary>
    /// Get the target user of a request.
    /// </summary>
    /// <param name="requestId">The App Portal request ID</param>
    /// <returns>Unique user name (domain\userid) of the target user of the request.</returns>
    [WebMethod(Description=@"Get the target user of a request.")]
    public string GetTargetUserForRequest(int requestId) {
        return GetVariableValue(requestId, "UniqueUserName");
    }

    /// <summary>
    /// Get all the devices assigned to the logged in user.
    /// </summary>
    /// <param name=""></param>
    /// <returns>List of devices</returns>
    [WebMethod(Description=@"Get all the devices assigned to the logged in user.")]
    public DataTable GetDevicesForLoggedInUser() {
        string userUniqueId = GetUniqueIdForLoggedInUser();
        SqlUtilities su = new SqlUtilities(ESDConfig.getConnString());
        string sqlQuery = String.Format(@"
                          SELECT MachineName
                          FROM vUserComputerMap
                          WHERE Active = 1 AND UniqueName = '{0}'", userUniqueId);
        DataTable dt = su.ExecuteSQLAndReturnDataSet(sqlQuery).Tables[0];
        Log.l1(string.Format("Found '{0}' device(s) for {1}", (dt.Rows.Count).ToString(), userUniqueId), LogFile, (int)LoggingLevel.VERBOSE);
        if (dt.Rows.Count > 0) {
            foreach (DataRow row in dt.Rows)
            {
                Log.l1(row.Field<string>(0), LogFile, (int)LoggingLevel.VERBOSE);
            }
        }
        return dt;
    }
 
    /// <summary>
    /// Get the unique id of the logged in user.
    /// </summary>
    /// <param name="">The user's unique user name.</param>
    /// <returns>The unique user name</returns>
    [WebMethod(Description =@"Get the unique ID (domain\username) of the logged in user.")]
    public string GetUniqueIdForLoggedInUser()
    {
        return System.Web.HttpContext.Current.User.Identity.Name;  // who is logged into the machine from which you are accessing App Portal. Assuming that the same user is logged into App Portal
        //string i = HttpContext.Current.Session("UniqueUserName").ToString();
        //return i;
    }

    /// <summary>
    /// Get all the devices assigned to a specific user.
    /// </summary>
    /// <param name="uniqueUserId">The user's unique user name (domain\userid).</param>
    /// <returns>List of devices</returns>
    [WebMethod(Description=@"Get all the devices assigned to a specific user.")]
    public DataTable GetDevicesForUser(string userUniqueId) {
        SqlUtilities su = new SqlUtilities(ESDConfig.getConnString());
        string sqlQuery = String.Format(@"
                          SELECT MachineName
                          FROM vUserComputerMap
                          WHERE Active = 1 AND UniqueName = '{0}'", userUniqueId);
        DataTable dt = su.ExecuteSQLAndReturnDataSet(sqlQuery).Tables[0];
        Log.l1(string.Format("Found '{0}' device(s) for {1}", (dt.Rows.Count).ToDbString(), userUniqueId), LogFile, (int)LoggingLevel.VERBOSE);
        return dt;
    }

    /// <summary>
    /// Get a specific property of the logged in user from the App Portal database.
    ///      The logged in user is also the requester.
    /// </summary>
    /// <param name="propertyName">The name of the user property.</param>
    /// <returns>The value of the user property.</returns>
    [WebMethod(Description=@"Get a specific AD property of the logged in user.")]
    public string GetLoggedInUserPropertyValue(string propertyName) {
        string userUniqueId = GetUniqueIdForLoggedInUser(); //@"flex\jsmith";
        string propertyValue = "";
        SqlUtilities su = new SqlUtilities(ESDConfig.getConnString());
        string sqlQuery = String.Format(@"
                          SELECT {0}
                          FROM WD_User
                          WHERE UniqueName = '{1}'", propertyName, userUniqueId);
        //Log.l1(string.Format("sqlQuery: {0}", sqlQuery), LogFile, (int)LoggingLevel.VERBOSE);
        propertyValue = su.ExecuteSQLAndReturnSingleValue(sqlQuery).ToDbString();
        Log.l1(string.Format("User: {0}, Property name: {1}, Value: {2}", userUniqueId, propertyName, propertyValue), LogFile, (int)LoggingLevel.VERBOSE);
        return propertyValue;
    }

    /// <summary>
    /// Get a specific property of the specified user from the App Portal database.
    /// </summary>
    /// <param name="propertyName">The name of the user property.</param>
    /// <param name="uniqueUserId">The user's unique user name (domain\userid).</param>
    /// <returns>The value of the user property.</returns>
    [WebMethod(Description=@"Get a specific AD property of a specific user.")]
    public string GetUserPropertyValue(string userUniqueId, string propertyName) {
        string propertyValue = "";
        SqlUtilities su = new SqlUtilities(ESDConfig.getConnString());
        string sqlQuery = String.Format(@"
                          SELECT {0}
                          FROM WD_User
                          WHERE UniqueName = '{1}'", propertyName, userUniqueId);
        //Log.l1(string.Format("sqlQuery: {0}", sqlQuery), LogFile, (int)LoggingLevel.VERBOSE);
        propertyValue = su.ExecuteSQLAndReturnSingleValue(sqlQuery).ToDbString();
        Log.l1(string.Format("User: {0}, Property name: {1}, Value: {2}", userUniqueId, propertyName, propertyValue), LogFile, (int)LoggingLevel.VERBOSE);
        return propertyValue;
    }

    /// <summary>
    /// Add user to or remove user from an AD group.
    /// </summary>
    /// <param name="strUserName">The AD user id.</param>
    /// <param name="strGroupName">The name of the AD group.</param>
    /// <param name="strAction">The action to perform.  Either 'add' or 'remove'.</param>
    /// <returns>The value of the user property.</returns>
    [WebMethod(Description=@"Add user to or remove user from an AD group.  Valid values for strAction are 'Add' or 'Remove' (without the quotes).  The default action is 'Add'.")]
    public int AddRemoveADGroupMember(string strUserName, string strGroupName, string strAction)
    {
        string strADAction = string.Empty;
        if (string.IsNullOrEmpty(strAction))
        {
            strADAction = "Add";
            Log.l1(string.Format("AD action/operation has not been specified.  '{0}' is the default operation", strADAction), LogFile, (int)LoggingLevel.VERBOSE);
        }
        else
        {
            // Make sure the action is in Title Case
            TextInfo ti = new CultureInfo("en-US",false).TextInfo;
            strADAction = string.Format("{0}{1}", ti.ToUpper(strAction[0]), ti.ToLower(strAction.Substring(1)));
        }

        Log.l1(string.Format("AddRemoveADGroupMember: {0} | {1} | {2}", strADAction, strUserName, strGroupName), LogFile, (int)LoggingLevel.VERBOSE);

        DirectoryServices ds = new DirectoryServices();

        // Get AD group GUID
        string groupguid = ds.getGUIDFromName(strGroupName);
        Log.l1(string.Format("  --GUID for AD Group '{0}': {1}", strGroupName, groupguid), LogFile, (int)LoggingLevel.VERBOSE);

        // Get AD user DN
        string userdn = ActiveDirectory.getDNFromName(strUserName);
        Log.l1(string.Format("  --DN for AD User '{0}': {1}", strUserName, userdn), LogFile, (int)LoggingLevel.VERBOSE);

        ActiveDirectory ad = new ActiveDirectory();
        try
        {
            // Add user to or remove user from group                    
            int oResult = ad.AddUserToGroup(userdn, groupguid, strADAction);
            if (oResult == 1)
            {
                Log.l1(string.Format("  --'{0}' operation was completed successfully", strADAction), LogFile, (int)LoggingLevel.VERBOSE);
                return oResult;
            }
            else
            {
                Log.l1(string.Format("  --'{0}' operation failed", strADAction), LogFile, (int)LoggingLevel.VERBOSE);
                return oResult;
            }
        }
        catch (Exception ex)
        {
            Log.l1(string.Format("  --Exception calling AddUserToGroup: {0}, {1}, {2}", strADAction, strUserName, strGroupName, ex), LogFile, (int)LoggingLevel.VERBOSE);
            Log.l("AddUserToGroup", ex, LogFile);
        }

        return 0;
    }

    /// <summary>
    /// Get the value of a specific catalog variable for the specified catalog item.
    /// </summary>
    /// <param name="packageId">The package ID of the desired catalog item (use 0 if getting a global catalog variable).</param>
    /// <param name="variableName">The name of the catalog variable to be retrieved (use the full name, including the prefix and underscore).</param>
    /// <returns>The value of the specified catalog variable.</returns>
    [WebMethod(Description=@"Get the value of a specific catalog variable on a specific catalog item. (Use packageId=0 for a global variable.  Use the full variableName, including the prefix and underscore - e.g. Custom_Template instead of Template.)")]
    public string GetCatalogVariable(int packageId, string variableName) {
        string variableValue = "";
        SqlUtilities su = new SqlUtilities(ESDConfig.getConnString());
        string sqlQuery = String.Format(@"
                          SELECT [Value]
                          FROM WD_CatalogVariable
                          WHERE [CatalogID] = {0} AND [Key] = '{1}'", packageId, variableName);
        //Log.l1(string.Format("sqlQuery: {0}", sqlQuery), LogFile, (int)LoggingLevel.VERBOSE);
        variableValue = su.ExecuteSQLAndReturnSingleValue(sqlQuery).ToDbString();
        Log.l1(string.Format("Catalog Item: [{0}], Variable Name: [{1}], Variable Value: [{2}]", packageId, variableName, variableValue), LogFile, (int)LoggingLevel.VERBOSE);
        return variableValue;
    }

    /// <summary>
    /// Set the value of a specific catalog variable for the specified catalog item.
    /// </summary>
    /// <param name="packageId">The package ID of the desired catalog item (use 0 if setting a global catalog variable).</param>
    /// <param name="variableName">The name of the catalog variable to be set (include the prefix and underscore).</param>
    /// <param name="variableValue">The value to be set for the specified catalog variable.</param>
    /// <param name="variableDescription">The description of the catalog variable to be set. [OPTIONAL]</param>
    /// <returns>"Success" if the variable was set successfully or an error message if the variable was not set.</returns>
    [WebMethod(Description=@"Set the value of a specific catalog variable on a specific catalog item. (Use packageId=0 for a global variable.  Include the prefix and underscore in the variableName - e.g. Custom_Template instead of Template.  variableDescription is optional.)")]
    public string SetCatalogVariable(int packageId, string variableName, string variableValue, string variableDescription="") {
        string retVal = "";

        if (string.IsNullOrEmpty(variableName)) {
            retVal = "Error: variableName must not be empty";
            Log.l1(retVal, LogFile);
            return retVal;
        }

        SqlUtilities su = new SqlUtilities(ESDConfig.getConnString());
        string sqlQuery = String.Format(@"
                          DECLARE @Order int
                          SET @Order = ISNULL((SELECT MAX([Order]) FROM WD_CatalogVariable WHERE [CatalogID]={0}) + 1, 0)

                          IF EXISTS (SELECT * FROM WD_CatalogVariable WHERE [CatalogID] = {0} AND [Key] = '{1}')
                              BEGIN
                                  UPDATE WD_CatalogVariable
                                  SET [Value]='{2}', [IsSaasCatalog]=0, [Description]='{3}'
                                  WHERE [CatalogID] = {0} AND [Key] = '{1}'
                              END
                          ELSE
                              BEGIN
                                  INSERT INTO WD_CatalogVariable ([Key], [Value], [Order], [CatalogID], [IsSaasCatalog], [Description])
                                  VALUES ('{1}', '{2}', @Order, {0}, 0, '{3}')
                              END", packageId, variableName, variableValue, variableDescription);
        //Log.l1(string.Format("sqlQuery: {0}", sqlQuery), LogFile, (int)LoggingLevel.VERBOSE);

        try {
            int rows = su.ExecuteSQL(sqlQuery);
            if (rows > 0) {
                retVal = "Success";
                Log.l1(string.Format("Successfully inserted or updated {0} row(s) in WD_CatalogVariable for Catalog Item: [{1}], Variable Name: [{2}], Variable Value: [{3}], Variable Description: [{4}]", rows, packageId, variableName, variableValue, variableDescription), LogFile, (int)LoggingLevel.VERBOSE);
                string userUniqueId = GetUniqueIdForLoggedInUser(); //@"flex\jsmith";
                sqlQuery = String.Format(@"
                           UPDATE WD_WebPackages
                           SET [UpdatedOn]=GETDATE(), [UpdatedBy]='{1}'
                           WHERE [PackageID] = {0}", packageId, userUniqueId);
                rows = su.ExecuteSQL(sqlQuery);
            }
            else {
                retVal = string.Format("Error: Unable to insert or update WD_CatalogVariable for Catalog Item: [{0}], Variable Name: [{1}]", packageId, variableName);
                Log.l1(retVal, LogFile);
            }
        }
        catch(Exception ex) {
            retVal = string.Format("Error: {0}", ex.Message);
            Log.l1(string.Format("Error: Unable to insert or update WD_CatalogVariable for Catalog Item: [{0}], Variable Name: [{1}]", packageId, variableName), LogFile);
            Log.l1(retVal, LogFile);
        }

        return retVal;
    }

    /// <summary>
    /// Add a message to the specified log file
    /// </summary>
    /// <param name="strMessage">The message to write to the log.</param>
    /// <param name="strLogFile">The log file to which the message will be written.</param>
    /// <returns>bool value indicating success/failure writing to the log.</returns>
    [WebMethod(Description=@"Add a message to the specified log file.")]
    public bool GCSLog(string strMessage, string strLogFile=LogFile)
    {
        try
        {
            Log.l1(strMessage, strLogFile);
        }
        catch
        {
            return false;
        }

        return true;
    }


    /// <summary>
    /// Add an error message to the specified log file and corresponding error log file
    /// </summary>
    /// <param name="strMessage">The message to write to the log.</param>
    /// <param name="strLogFile">The log file to which the message will be written.</param>
    /// <returns>bool value indicating success/failure writing to the log.</returns>
    [WebMethod(Description=@"Add an error message to the specified log file and corresponding error log file.")]
    public bool GCSLogError(string strMessage, string strLogFile=LogFile)
    {
        string strErrorLogFile = strLogFile.Substring(0,strLogFile.Length-4) + @"_Error.log";

        try
        {
            Log.l1(strMessage, strLogFile);
            Log.l1(strMessage, strErrorLogFile);
        }
        catch
        {
            return false;
        }

        return true;
    }


    /// <summary>
    /// Get the value of an App Broker setting.
    /// </summary>
    /// <param name="strSetting">The name of the desired App Broker setting.</param>
    /// <returns>The value of and App Broker setting.</returns>
    [WebMethod(Description=@"Get the value of an App Broker setting.")]
    public string GetABSetting(string strSetting)
    {
        if (strSetting=="ConnectionString")
        {
            return ESDConfig.getConnString();
        }
        return ESDConfig.getConfigValue(strSetting).ToDbString();
    }


    /// <summary>
    /// Get MachineDomain for a device from FNMS.
    /// </summary>
    /// <param name="strMachineName">The name of the device for which to look up the domain.</param>
    /// <returns>The domain "flat name" for the specified device.</returns>
    [WebMethod(Description=@"Get all inventory devices from FNMS.")]
    public string GetMachineDomainFromFNMS(string strMachineName) {
        string strMachineDomain = "";

        string fnmsServer = ESDConfig.getConfigValue("FNMSDBServer").ToDbString();
        string fnmsDatabase = ESDConfig.getConfigValue("FNMSDBName").ToDbString();
        string fnmsConnStr = String.Format("Data Source={0};Initial Catalog={1};Integrated Security=True;", fnmsServer, fnmsDatabase);

        SqlUtilities su = new SqlUtilities(fnmsConnStr);
        string sqlQuery = String.Format(@"
                          SELECT cd.FlatName AS [MachineDomain]
                          FROM ComplianceComputer cc
                          JOIN ComplianceDomain cd ON cc.ComplianceDomainID = cd.ComplianceDomainID
                          WHERE cc.ComputerName='{0}'", strMachineName);
        strMachineDomain = su.ExecuteSQLAndReturnSingleValue(sqlQuery).ToDbString();
        Log.l1(string.Format("MachineName: [{0}], MachineDomain: [{1}]", strMachineName, strMachineDomain), LogFile, (int)LoggingLevel.VERBOSE);
        return strMachineDomain;
    }

}

 

Anything expressed here is my own view and not necessarily that of my employer, Flexera. If my reply answers a question you have raised, please click "ACCEPT AS SOLUTION".

Thank you both for your quick answers, I'll try the PowerShell script first, as it seems to be more "leightweight" and easier to manage, atleast for me. If I get it to work I'll mark it as solution otherwise I probably come back for more questions. 😉