A new Flexera Community experience is coming on November 25th. Click here for more information.
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:
"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:
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
Jun 21, 2022 07:57 AM - edited Jun 21, 2022 08:34 AM
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..
Jun 21, 2022 10:02 AM
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..
Jun 21, 2022 10:02 AM
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;
}
}
Jun 21, 2022 07:20 PM
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. 😉
Jun 22, 2022 01:47 AM
@Tipadu - you can also vote up this 'Idea' - Native functionality to remove user or machine from AD Group | Ideas (aha.io)
Jul 07, 2022 12:21 PM