From c2a5c0e693aa4c291a38e412f88b84e56e3b1d54 Mon Sep 17 00:00:00 2001 From: Bevan Weiss Date: Tue, 18 Jun 2024 19:03:40 +1000 Subject: [PATCH] CreateGroups additions Signed-off-by: Bevan Weiss --- src/ext/Firewall/ca/CustomMsiErrors.h | 4 + src/ext/Iis/ca/sca.h | 12 + src/ext/Util/ca/CustomMsiErrors.h | 6 +- src/ext/Util/ca/sca.h | 12 + src/ext/Util/ca/scacost.h | 2 + src/ext/Util/ca/scaexec.cpp | 634 +++++++++++++++++- src/ext/Util/ca/scagroup.cpp | 503 ++++++++++++++ src/ext/Util/ca/scagroup.h | 47 ++ src/ext/Util/ca/scanet.cpp | 50 ++ src/ext/Util/ca/scanet.h | 4 + src/ext/Util/ca/scasched.cpp | 47 +- src/ext/Util/ca/scauser.cpp | 83 --- src/ext/Util/ca/scauser.h | 20 +- src/ext/Util/ca/utilca.def | 4 + src/ext/Util/ca/utilca.vcxproj | 4 + .../TestData/CreateGroup/Package.wxs | 15 + .../CreateGroup/PackageComponents.wxs | 80 +++ .../UtilExtensionFixture.cs | 153 +++++ .../Util/wixext/Symbols/GroupGroupSymbol.cs | 56 ++ src/ext/Util/wixext/Symbols/GroupSymbol.cs | 97 ++- .../wixext/Symbols/UtilSymbolDefinitions.cs | 8 + src/ext/Util/wixext/UtilCompiler.cs | 141 +++- src/ext/Util/wixext/UtilDecompiler.cs | 62 +- src/ext/Util/wixext/UtilTableDefinitions.cs | 25 + src/ext/Util/wixlib/UtilExtension.wxs | 8 + .../Util/wixlib/UtilExtension_Platform.wxi | 14 + src/ext/Util/wixlib/de-de.wxl | 4 + src/ext/Util/wixlib/en-us.wxl | 4 + src/ext/Util/wixlib/es-es.wxl | 4 + src/ext/Util/wixlib/fr-fr.wxl | 4 + src/ext/Util/wixlib/it-it.wxl | 4 + src/ext/Util/wixlib/ja-jp.wxl | 4 + src/ext/Util/wixlib/nl-nl.wxl | 4 + src/ext/Util/wixlib/pt-br.wxl | 4 + src/ext/caerr.wxi | 3 + .../WixToolset.WcaUtil/custommsierrors.h | 3 + .../burn/WixTestTools/UserGroupVerifier.cs | 198 ++++++ .../ProductA/ProductA.wixproj | 13 + .../ProductA/product.wxs | 25 + .../ProductAddCommentToExistingGroup.wixproj | 13 + .../product.wxs | 23 + .../ProductCommentDelete.wixproj | 13 + .../ProductCommentDelete/product.wxs | 18 + .../ProductCommentFail.wixproj | 13 + .../ProductCommentFail/product_fail.wxs | 22 + .../ProductFail/ProductFail.wixproj | 13 + .../ProductFail/product_fail.wxs | 29 + .../ProductFailIfExists/FailIfExists.wxs | 18 + .../ProductFailIfExists.wixproj | 13 + .../ProductNestedGroups.wixproj | 13 + .../ProductNestedGroups/product.wxs | 33 + .../ProductNewGroupWithComment.wixproj | 13 + .../ProductNewGroupWithComment/product.wxs | 23 + .../NonVitalUserGroup.wxs | 19 + .../ProductNonVitalUserGroup.wixproj | 13 + .../ProductRestrictedDomain.wixproj | 13 + .../RestrictedDomain.wxs | 20 + .../ProductWithCommandLineParameters.wixproj | 13 + .../ProductWithCommandLineParameters.wxs | 19 + .../UtilExtensionGroupTests.cs | 271 ++++++++ 60 files changed, 2833 insertions(+), 157 deletions(-) create mode 100644 src/ext/Util/ca/scagroup.cpp create mode 100644 src/ext/Util/ca/scagroup.h create mode 100644 src/ext/Util/ca/scanet.cpp create mode 100644 src/ext/Util/ca/scanet.h create mode 100644 src/ext/Util/test/WixToolsetTest.Util/TestData/CreateGroup/Package.wxs create mode 100644 src/ext/Util/test/WixToolsetTest.Util/TestData/CreateGroup/PackageComponents.wxs create mode 100644 src/ext/Util/wixext/Symbols/GroupGroupSymbol.cs create mode 100644 src/test/burn/WixTestTools/UserGroupVerifier.cs create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductA/ProductA.wixproj create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductA/product.wxs create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductAddCommentToExistingGroup/ProductAddCommentToExistingGroup.wixproj create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductAddCommentToExistingGroup/product.wxs create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductCommentDelete/ProductCommentDelete.wixproj create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductCommentDelete/product.wxs create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductCommentFail/ProductCommentFail.wixproj create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductCommentFail/product_fail.wxs create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductFail/ProductFail.wixproj create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductFail/product_fail.wxs create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductFailIfExists/FailIfExists.wxs create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductFailIfExists/ProductFailIfExists.wixproj create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductNestedGroups/ProductNestedGroups.wixproj create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductNestedGroups/product.wxs create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductNewGroupWithComment/ProductNewGroupWithComment.wixproj create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductNewGroupWithComment/product.wxs create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductNonVitalGroup/NonVitalUserGroup.wxs create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductNonVitalGroup/ProductNonVitalUserGroup.wixproj create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductRestrictedDomain/ProductRestrictedDomain.wixproj create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductRestrictedDomain/RestrictedDomain.wxs create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductWithCommandLineParameters/ProductWithCommandLineParameters.wixproj create mode 100644 src/test/msi/TestData/UtilExtensionGroupTests/ProductWithCommandLineParameters/ProductWithCommandLineParameters.wxs create mode 100644 src/test/msi/WixToolsetTest.MsiE2E/UtilExtensionGroupTests.cs diff --git a/src/ext/Firewall/ca/CustomMsiErrors.h b/src/ext/Firewall/ca/CustomMsiErrors.h index f149fb314..6a7f4b3c2 100644 --- a/src/ext/Firewall/ca/CustomMsiErrors.h +++ b/src/ext/Firewall/ca/CustomMsiErrors.h @@ -87,6 +87,10 @@ #define msierrUSRFailedUserCreateExists 26404 #define msierrUSRFailedGrantLogonAsService 26405 +#define msierrGRPFailedGroupCreate 26421 +#define msierrGRPFailedGroupGroupAdd 26422 +#define msierrGRPFailedGroupCreateExists 26423 + #define msierrDependencyMissingDependencies 26451 #define msierrDependencyHasDependents 26452 diff --git a/src/ext/Iis/ca/sca.h b/src/ext/Iis/ca/sca.h index 6921613b5..b97e9a7eb 100644 --- a/src/ext/Iis/ca/sca.h +++ b/src/ext/Iis/ca/sca.h @@ -123,3 +123,15 @@ enum SCAU_ATTRIBUTES SCAU_NON_VITAL = 0x00000400, SCAU_REMOVE_COMMENT = 0x00000800, }; + +// group creation attributes definitions +enum SCAG_ATTRIBUTES +{ + SCAG_FAIL_IF_EXISTS = 0x00000001, + SCAG_UPDATE_IF_EXISTS = 0x00000002, + + SCAG_DONT_REMOVE_ON_UNINSTALL = 0x00000004, + SCAG_DONT_CREATE_GROUP = 0x00000008, + SCAG_NON_VITAL = 0x00000010, + SCAG_REMOVE_COMMENT = 0x00000020, +}; diff --git a/src/ext/Util/ca/CustomMsiErrors.h b/src/ext/Util/ca/CustomMsiErrors.h index 3218b61be..ac0c549f6 100644 --- a/src/ext/Util/ca/CustomMsiErrors.h +++ b/src/ext/Util/ca/CustomMsiErrors.h @@ -29,4 +29,8 @@ #define msierrUSRFailedUserCreateExists 26404 #define msierrUSRFailedGrantLogonAsService 26405 -//Last available is 26450 \ No newline at end of file +#define msierrGRPFailedGroupCreate 26421 +#define msierrGRPFailedGroupGroupAdd 26422 +#define msierrGRPFailedGroupCreateExists 26423 + +//Last available is 26450 diff --git a/src/ext/Util/ca/sca.h b/src/ext/Util/ca/sca.h index 84f5ffd92..0e25a19ab 100644 --- a/src/ext/Util/ca/sca.h +++ b/src/ext/Util/ca/sca.h @@ -18,3 +18,15 @@ enum SCAU_ATTRIBUTES SCAU_NON_VITAL = 0x00000400, SCAU_REMOVE_COMMENT = 0x00000800, }; + +// group creation attributes definitions +enum SCAG_ATTRIBUTES +{ + SCAG_FAIL_IF_EXISTS = 0x00000001, + SCAG_UPDATE_IF_EXISTS = 0x00000002, + + SCAG_DONT_REMOVE_ON_UNINSTALL = 0x00000004, + SCAG_DONT_CREATE_GROUP = 0x00000008, + SCAG_NON_VITAL = 0x00000010, + SCAG_REMOVE_COMMENT = 0x00000020, +}; diff --git a/src/ext/Util/ca/scacost.h b/src/ext/Util/ca/scacost.h index 5b215035f..978e40bc3 100644 --- a/src/ext/Util/ca/scacost.h +++ b/src/ext/Util/ca/scacost.h @@ -9,6 +9,8 @@ const UINT COST_SMB_CREATESMB = 10000; const UINT COST_SMB_DROPSMB = 5000; const UINT COST_USER_ADD = 10000; const UINT COST_USER_DELETE = 10000; +const UINT COST_GROUP_ADD = 10000; +const UINT COST_GROUP_DELETE = 10000; const UINT COST_PERFMONMANIFEST_REGISTER = 1000; const UINT COST_PERFMONMANIFEST_UNREGISTER = 1000; diff --git a/src/ext/Util/ca/scaexec.cpp b/src/ext/Util/ca/scaexec.cpp index 5119bc11e..cc54150ba 100644 --- a/src/ext/Util/ca/scaexec.cpp +++ b/src/ext/Util/ca/scaexec.cpp @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. #include "precomp.h" +#include "scanet.h" /******************************************************************** * CreateSmb - CUSTOM ACTION ENTRY POINT for creating fileshares @@ -154,6 +155,7 @@ extern "C" UINT __stdcall DropSmb(MSIHANDLE hInstall) } + static HRESULT AddUserToGroup( __in LPWSTR wzUser, __in LPCWSTR wzUserDomain, @@ -291,6 +293,145 @@ static HRESULT RemoveUserFromGroup( return hr; } +static HRESULT AddGroupToGroup( + __in LPWSTR wzMember, + __in LPCWSTR wzMemberDomain, + __in LPCWSTR wzGroup, + __in LPCWSTR wzGroupDomain +) +{ + Assert(wzMember && *wzMember && wzMemberDomain && wzGroup && *wzGroup && wzGroupDomain); + + HRESULT hr = S_OK; + IADsGroup* pGroup = NULL; + BSTR bstrMember = NULL; + BSTR bstrGroup = NULL; + LPWSTR pwzMember = NULL; + LPWSTR pwzServerName = NULL; + LOCALGROUP_MEMBERS_INFO_3 lgmi {}; + + GetDomainServerName(wzGroupDomain, &pwzServerName); + + // Try adding it to the local group + if (wzMemberDomain) + { + hr = StrAllocFormatted(&pwzMember, L"%s\\%s", wzMemberDomain, wzMember); + ExitOnFailure(hr, "failed to allocate group domain string"); + } + + lgmi.lgrmi3_domainandname = (NULL == pwzMember ? wzMember : pwzMember); + NET_API_STATUS ui = ::NetLocalGroupAddMembers(pwzServerName, wzGroup, 3, reinterpret_cast(&lgmi), 1); + hr = HRESULT_FROM_WIN32(ui); + if (HRESULT_FROM_WIN32(ERROR_MEMBER_IN_ALIAS) == hr) // if they're already a member of the group don't report an error + { + hr = S_OK; + } + + // + // If we failed, try active directory + // + if (FAILED(hr)) + { + WcaLog(LOGMSG_VERBOSE, "Failed to add group: %ls, domain %ls to group: %ls, domain: %ls with error 0x%x. Attempting to use Active Directory", wzMember, wzMemberDomain, wzGroup, wzGroupDomain, hr); + + hr = UserCreateADsPath(wzMemberDomain, wzMember, &bstrMember); + ExitOnFailure(hr, "failed to create group ADsPath for group: %ls domain: %ls", wzMember, wzMemberDomain); + + hr = UserCreateADsPath(wzGroupDomain, wzGroup, &bstrGroup); + ExitOnFailure(hr, "failed to create group ADsPath for group: %ls domain: %ls", wzGroup, wzGroupDomain); + + hr = ::ADsGetObject(bstrGroup, IID_IADsGroup, reinterpret_cast(&pGroup)); + ExitOnFailure(hr, "Failed to get group '%ls'.", reinterpret_cast(bstrGroup)); + + hr = pGroup->Add(bstrMember); + if ((HRESULT_FROM_WIN32(ERROR_OBJECT_ALREADY_EXISTS) == hr) || (HRESULT_FROM_WIN32(ERROR_MEMBER_IN_ALIAS) == hr)) + hr = S_OK; + + ExitOnFailure(hr, "Failed to add group %ls to group '%ls'.", reinterpret_cast(bstrMember), reinterpret_cast(bstrGroup)); + } + +LExit: + ReleaseStr(pwzServerName); + ReleaseStr(pwzMember); + ReleaseBSTR(bstrMember); + ReleaseBSTR(bstrGroup); + ReleaseObject(pGroup); + + return hr; +} + +static HRESULT RemoveGroupFromGroup( + __in LPWSTR wzMember, + __in LPCWSTR wzMemberDomain, + __in LPCWSTR wzGroup, + __in LPCWSTR wzGroupDomain +) +{ + Assert(wzMember && *wzMember && wzMemberDomain && wzGroup && *wzGroup && wzGroupDomain); + + HRESULT hr = S_OK; + IADsGroup* pGroup = NULL; + BSTR bstrMember = NULL; + BSTR bstrGroup = NULL; + LPWSTR pwzMember = NULL; + LPWSTR pwzServerName = NULL; + LOCALGROUP_MEMBERS_INFO_3 lgmi {}; + + GetDomainServerName(wzGroupDomain, &pwzServerName, DS_WRITABLE_REQUIRED); + + // Try removing it from the local group + if (wzMemberDomain) + { + hr = StrAllocFormatted(&pwzMember, L"%s\\%s", wzMemberDomain, wzMember); + ExitOnFailure(hr, "failed to allocate group domain string"); + } + + lgmi.lgrmi3_domainandname = (NULL == pwzMember ? wzMember : pwzMember); + NET_API_STATUS ui = ::NetLocalGroupDelMembers(pwzServerName, wzGroup, 3, reinterpret_cast(&lgmi), 1); + hr = HRESULT_FROM_WIN32(ui); + if (HRESULT_FROM_WIN32(ERROR_MEMBER_NOT_IN_ALIAS) == hr + || HRESULT_FROM_WIN32(NERR_GroupNotFound) == hr + || HRESULT_FROM_WIN32(ERROR_NO_SUCH_MEMBER) == hr) // if they're already not a member of the group, or the group doesn't exist, don't report an error + { + hr = S_OK; + } + + // + // If we failed, try active directory + // + if (FAILED(hr)) + { + WcaLog(LOGMSG_VERBOSE, "Failed to remove group: %ls, domain %ls from group: %ls, domain: %ls with error 0x%x. Attempting to use Active Directory", wzMember, wzMemberDomain, wzGroup, wzGroupDomain, hr); + + hr = UserCreateADsPath(wzMemberDomain, wzMember, &bstrMember); + ExitOnFailure(hr, "failed to create group ADsPath in order to remove group: %ls domain: %ls from a group", wzMember, wzMemberDomain); + + hr = UserCreateADsPath(wzGroupDomain, wzGroup, &bstrGroup); + ExitOnFailure(hr, "failed to create group ADsPath in order to remove group from group: %ls domain: %ls", wzGroup, wzGroupDomain); + + hr = ::ADsGetObject(bstrGroup, IID_IADsGroup, reinterpret_cast(&pGroup)); + if ((HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) == hr)) // if parent group not found, no need to remove membership from group + { + hr = S_OK; + ExitFunction(); + } + ExitOnFailure(hr, "Failed to get group '%ls'.", reinterpret_cast(bstrGroup)); + + hr = pGroup->Remove(bstrMember); + if ((HRESULT_FROM_WIN32(ERROR_MEMBER_NOT_IN_ALIAS) == hr)) // if already not a member, no need to worry about error + hr = S_OK; + ExitOnFailure(hr, "Failed to remove group %ls from group '%ls'.", reinterpret_cast(bstrMember), reinterpret_cast(bstrGroup)); + } + +LExit: + ReleaseStr(pwzServerName); + ReleaseStr(pwzMember); + ReleaseBSTR(bstrMember); + ReleaseBSTR(bstrGroup); + ReleaseObject(pGroup); + + return hr; +} static HRESULT GetUserHasRight( __in LSA_HANDLE hPolicy, @@ -590,6 +731,15 @@ static HRESULT SetUserComment(__in LPWSTR pwzServerName, __in LPWSTR pwzName, __ return HRESULT_FROM_WIN32(er); } +static HRESULT SetGroupComment(__in LPWSTR pwzServerName, __in LPWSTR pwzName, __in LPWSTR pwzComment) +{ + _LOCALGROUP_INFO_1002 groupInfo1002 {}; + + groupInfo1002.lgrpi1002_comment = pwzComment; + NET_API_STATUS er = ::NetLocalGroupSetInfo(pwzServerName, pwzName, 1002, reinterpret_cast(&groupInfo1002), NULL); + return HRESULT_FROM_WIN32(er); +} + static HRESULT SetUserFlags(__in LPWSTR pwzServerName, __in LPWSTR pwzName, __in DWORD flags) { NET_API_STATUS er = NERR_Success; @@ -715,44 +865,76 @@ static HRESULT RemoveUserInternal( return hr; } -static void GetServerName(LPWSTR pwzDomain, LPWSTR* ppwzServerName) +static HRESULT RemoveGroupInternal( + LPWSTR wzGroupCaData, + LPWSTR wzDomain, + LPWSTR wzName, + int iAttributes +) { - DWORD er = ERROR_SUCCESS; - PDOMAIN_CONTROLLER_INFOW pDomainControllerInfo = NULL; + HRESULT hr = S_OK; - if (pwzDomain && *pwzDomain) + LPWSTR pwz = NULL; + LPWSTR pwzGroup = NULL; + LPWSTR pwzGroupDomain = NULL; + LPWSTR pwzServerName = NULL; + + // + // Remove the Group if the group was created by us. + // + if (!(SCAG_DONT_CREATE_GROUP & iAttributes)) { - er = ::DsGetDcNameW(NULL, (LPCWSTR)pwzDomain, NULL, NULL, NULL, &pDomainControllerInfo); - if (RPC_S_SERVER_UNAVAILABLE == er) + GetDomainServerName(wzDomain, &pwzServerName, DS_WRITABLE_REQUIRED); + + NET_API_STATUS er = ::NetLocalGroupDel(pwzServerName, wzName); + hr = HRESULT_FROM_WIN32(er); + if (HRESULT_FROM_WIN32(ERROR_NO_SUCH_ALIAS) == hr + || HRESULT_FROM_WIN32(NERR_GroupNotFound) == hr) // we wanted to delete it.. and the group doesn't exist.. solved. { - // MSDN says, if we get the above error code, try again with the "DS_FORCE_REDISCOVERY" flag - er = ::DsGetDcNameW(NULL, (LPCWSTR)pwzDomain, NULL, NULL, DS_FORCE_REDISCOVERY, &pDomainControllerInfo); + hr = S_OK; } - - if (ERROR_SUCCESS == er && pDomainControllerInfo->DomainControllerName) + ExitOnFailure(hr, "failed to delete group: %ls", wzName); + } + else + { + // + // Remove the group from other groups + // + pwz = wzGroupCaData; + while (S_OK == (hr = WcaReadStringFromCaData(&pwz, &pwzGroup))) { - // Skip the \\ prefix if present. - if ('\\' == *pDomainControllerInfo->DomainControllerName && '\\' == *pDomainControllerInfo->DomainControllerName + 1) + hr = WcaReadStringFromCaData(&pwz, &pwzGroupDomain); + + if (FAILED(hr)) { - *ppwzServerName = pDomainControllerInfo->DomainControllerName + 2; + WcaLogError(hr, "failed to get domain for group: %ls, continuing anyway.", pwzGroup); } else { - *ppwzServerName = pDomainControllerInfo->DomainControllerName; + hr = RemoveGroupFromGroup(wzName, wzDomain, pwzGroup, pwzGroupDomain); + if (FAILED(hr)) + { + WcaLogError(hr, "failed to remove group: %ls from group %ls, continuing anyway.", wzName, pwzGroup); + } } } - else + + if (E_NOMOREITEMS == hr) // if there are no more items, all is well { - *ppwzServerName = pwzDomain; + hr = S_OK; } - } - if (pDomainControllerInfo) - { - ::NetApiBufferFree((LPVOID)pDomainControllerInfo); + ExitOnFailure(hr, "failed to get next group from which to remove group:%ls", wzName); } +LExit: + ReleaseStr(pwzServerName); + ReleaseStr(pwzGroup); + ReleaseStr(pwzGroupDomain); + + return hr; } + /******************************************************************** CreateUser - CUSTOM ACTION ENTRY POINT for creating users @@ -845,7 +1027,7 @@ extern "C" UINT __stdcall CreateUser( // // Create the User // - GetServerName(pwzDomain, &pwzServerName); + GetDomainServerName(pwzDomain, &pwzServerName); er = ::NetUserAdd(pwzServerName, 1, reinterpret_cast(pUserInfo1), &dw); if (NERR_UserExists == er) @@ -1231,3 +1413,413 @@ extern "C" UINT __stdcall RemoveUser( return WcaFinalize(er); } + +/******************************************************************** + CreateGroup - CUSTOM ACTION ENTRY POINT for creating groups + + Input: deferred CustomActionData - GroupName\tDomain\tComment\tAttributes + * *****************************************************************/ +extern "C" UINT __stdcall CreateGroup( + __in MSIHANDLE hInstall +) +{ + //AssertSz(0, "Debug CreateGroup"); + + HRESULT hr = S_OK; + NET_API_STATUS er = ERROR_SUCCESS; + + LPWSTR pwzData = NULL; + LPWSTR pwz = NULL; + LPWSTR pwzName = NULL; + LPWSTR pwzDomain = NULL; + LPWSTR pwzComment = NULL; + LPWSTR pwzScriptKey = NULL; + LPWSTR pwzGroup = NULL; + LPWSTR pwzGroupDomain = NULL; + int iAttributes = 0; + BOOL fInitializedCom = FALSE; + + LOCALGROUP_INFO_1* pGroupInfo1 = NULL; + + WCA_CASCRIPT_HANDLE hRollbackScript = NULL; + int iRollbackAttributes = 0; + + DWORD dw; + LPWSTR pwzServerName = NULL; + + hr = WcaInitialize(hInstall, "CreateGroup"); + ExitOnFailure(hr, "failed to initialize"); + + hr = ::CoInitialize(NULL); + ExitOnFailure(hr, "failed to initialize COM"); + fInitializedCom = TRUE; + + hr = WcaGetProperty(L"CustomActionData", &pwzData); + ExitOnFailure(hr, "failed to get CustomActionData"); + + WcaLog(LOGMSG_TRACEONLY, "CustomActionData: %ls", pwzData); + + // + // Read in the CustomActionData + // + pwz = pwzData; + hr = WcaReadStringFromCaData(&pwz, &pwzName); + ExitOnFailure(hr, "failed to read group name from custom action data"); + + hr = WcaReadStringFromCaData(&pwz, &pwzDomain); + ExitOnFailure(hr, "failed to read domain from custom action data"); + + hr = WcaReadStringFromCaData(&pwz, &pwzComment); + ExitOnFailure(hr, "failed to read group comment from custom action data"); + + hr = WcaReadIntegerFromCaData(&pwz, &iAttributes); + ExitOnFailure(hr, "failed to read attributes from custom action data"); + + hr = WcaReadStringFromCaData(&pwz, &pwzScriptKey); + ExitOnFailure(hr, "failed to read encoding key from custom action data"); + + if (!(SCAG_DONT_CREATE_GROUP & iAttributes)) + { + hr = GetDomainServerName(pwzDomain, &pwzServerName, DS_WRITABLE_REQUIRED); + ExitOnFailure(hr, "failed to find Domain %ls.", pwzDomain); + + // Set the group's comment + if (SCAG_REMOVE_COMMENT & iAttributes) + { + StrAllocString(&pwzComment, L"", 0); + } + + // + // Create the Group + // + LOCALGROUP_INFO_1 groupInfo1; + groupInfo1.lgrpi1_name = pwzName; + groupInfo1.lgrpi1_comment = pwzComment; + er = ::NetLocalGroupAdd(pwzServerName, 1, reinterpret_cast(&groupInfo1), &dw); + hr = HRESULT_FROM_WIN32(er); + + if (HRESULT_FROM_WIN32(ERROR_ALIAS_EXISTS) == hr + || HRESULT_FROM_WIN32(NERR_GroupExists) == hr) + { + if (SCAG_FAIL_IF_EXISTS & iAttributes) + { + MessageExitOnFailure(hr, msierrGRPFailedGroupCreateExists, "Group (%ls) was not supposed to exist, but does", pwzName); + } + + hr = S_OK; // Make sure that we don't report this situation as an error + // if we fall through the tests that follow. + + if (SCAG_UPDATE_IF_EXISTS & iAttributes) + { + er = ::NetLocalGroupGetInfo(pwzServerName, pwzName, 1, reinterpret_cast(&pGroupInfo1)); + hr = HRESULT_FROM_WIN32(er); + if (S_OK == hr) + { + // There is no rollback scheduled if the key is empty. + // Best effort to get original configuration and save it in the script so rollback can restore it. + if (*pwzScriptKey) + { + // Try to open the rollback script + hr = WcaCaScriptOpen(WCA_ACTION_INSTALL, WCA_CASCRIPT_ROLLBACK, FALSE, pwzScriptKey, &hRollbackScript); + + if (hRollbackScript && INVALID_HANDLE_VALUE != hRollbackScript->hScriptFile) + { + WcaCaScriptClose(hRollbackScript, WCA_CASCRIPT_CLOSE_PRESERVE); + } + else + { + hRollbackScript = NULL; + hr = WcaCaScriptCreate(WCA_ACTION_INSTALL, WCA_CASCRIPT_ROLLBACK, FALSE, pwzScriptKey, FALSE, &hRollbackScript); + ExitOnFailure(hr, "Failed to open rollback CustomAction script."); + + iRollbackAttributes = 0; + + hr = WcaCaScriptWriteString(hRollbackScript, pGroupInfo1->lgrpi1_comment); + ExitOnFailure(hr, "Failed to add rollback comment to rollback script."); + + if (!pGroupInfo1->lgrpi1_comment || !*pGroupInfo1->lgrpi1_comment) + { + iRollbackAttributes |= SCAG_REMOVE_COMMENT; + } + + hr = WcaCaScriptWriteNumber(hRollbackScript, iRollbackAttributes); + ExitOnFailure(hr, "Failed to add rollback attributes to rollback script."); + + // Nudge the system to get all our rollback data written to disk. + WcaCaScriptFlush(hRollbackScript); + } + } + } + + if (S_OK == hr) + { + if (SCAG_REMOVE_COMMENT & iAttributes) + { + hr = SetGroupComment(pwzServerName, pwzName, L""); + if (FAILED(hr)) + { + WcaLogError(hr, "failed to clear comment for group %ls\\%ls, continuing anyway.", pwzServerName, pwzName); + hr = S_OK; + } + } + else if (pwzComment && *pwzComment) + { + hr = SetGroupComment(pwzServerName, pwzName, pwzComment); + if (FAILED(hr)) + { + WcaLogError(hr, "failed to set comment to %ls for group %ls\\%ls, continuing anyway.", pwzComment, pwzServerName, pwzName); + hr = S_OK; + } + } + } + } + } + MessageExitOnFailure(hr, msierrGRPFailedGroupCreate, "failed to create group: %ls", pwzName); + + // + // Add the groups to groups + // + while (S_OK == (hr = WcaReadStringFromCaData(&pwz, &pwzGroup))) + { + hr = WcaReadStringFromCaData(&pwz, &pwzGroupDomain); + ExitOnFailure(hr, "failed to get domain for group: %ls", pwzGroup); + + WcaLog(LOGMSG_STANDARD, "Adding group %ls\\%ls to group %ls\\%ls", pwzDomain, pwzName, pwzGroupDomain, pwzGroup); + hr = AddGroupToGroup(pwzName, pwzDomain, pwzGroup, pwzGroupDomain); + MessageExitOnFailure(hr, msierrUSRFailedUserGroupAdd, "failed to add group: %ls to group %ls", pwzName, pwzGroup); + } + if (E_NOMOREITEMS == hr) // if there are no more items, all is well + { + hr = S_OK; + } + ExitOnFailure(hr, "failed to get next group in which to include group: %ls", pwzName); + } + +LExit: + WcaCaScriptClose(hRollbackScript, WCA_CASCRIPT_CLOSE_PRESERVE); + + if (pGroupInfo1) + { + ::NetApiBufferFree((LPVOID)pGroupInfo1); + } + + ReleaseStr(pwzData); + ReleaseStr(pwzName); + ReleaseStr(pwzDomain); + ReleaseStr(pwzComment); + ReleaseStr(pwzScriptKey); + ReleaseStr(pwzGroup); + ReleaseStr(pwzGroupDomain); + + if (fInitializedCom) + { + ::CoUninitialize(); + } + + if (SCAG_NON_VITAL & iAttributes) + { + er = ERROR_SUCCESS; + } + else if (FAILED(hr)) + { + er = ERROR_INSTALL_FAILURE; + } + + return WcaFinalize(er); +} + + +/******************************************************************** + CreateGroupRollback - CUSTOM ACTION ENTRY POINT for CreateGroup rollback + + * *****************************************************************/ +extern "C" UINT __stdcall CreateGroupRollback( + MSIHANDLE hInstall +) +{ + //AssertSz(0, "Debug CreateGroupRollback"); + + HRESULT hr = S_OK; + UINT er = ERROR_SUCCESS; + + LPWSTR pwzData = NULL; + LPWSTR pwz = NULL; + LPWSTR pwzScriptKey = NULL; + LPWSTR pwzName = NULL; + LPWSTR pwzDomain = NULL; + LPWSTR pwzComment = NULL; + int iAttributes = 0; + BOOL fInitializedCom = FALSE; + + WCA_CASCRIPT_HANDLE hRollbackScript = NULL; + LPWSTR pwzRollbackData = NULL; + int iOriginalAttributes = 0; + LPWSTR pwzOriginalComment = NULL; + + hr = WcaInitialize(hInstall, "CreateGroupRollback"); + ExitOnFailure(hr, "failed to initialize"); + + hr = ::CoInitialize(NULL); + ExitOnFailure(hr, "failed to initialize COM"); + fInitializedCom = TRUE; + + hr = WcaGetProperty(L"CustomActionData", &pwzData); + ExitOnFailure(hr, "failed to get CustomActionData"); + + WcaLog(LOGMSG_TRACEONLY, "CustomActionData: %ls", pwzData); + + // + // Read in the CustomActionData + // + pwz = pwzData; + hr = WcaReadStringFromCaData(&pwz, &pwzScriptKey); + ExitOnFailure(hr, "failed to read encoding key from custom action data"); + + hr = WcaReadStringFromCaData(&pwz, &pwzName); + ExitOnFailure(hr, "failed to read name from custom action data"); + + hr = WcaReadStringFromCaData(&pwz, &pwzDomain); + ExitOnFailure(hr, "failed to read domain from custom action data"); + + hr = WcaReadStringFromCaData(&pwz, &pwzComment); + ExitOnFailure(hr, "failed to read comment from custom action data"); + + hr = WcaReadIntegerFromCaData(&pwz, &iAttributes); + ExitOnFailure(hr, "failed to read attributes from custom action data"); + + // Best effort to read original configuration from CreateUser. + hr = WcaCaScriptOpen(WCA_ACTION_INSTALL, WCA_CASCRIPT_ROLLBACK, FALSE, pwzScriptKey, &hRollbackScript); + if (FAILED(hr)) + { + WcaLogError(hr, "Failed to open rollback CustomAction script, continuing anyway."); + } + else + { + hr = WcaCaScriptReadAsCustomActionData(hRollbackScript, &pwzRollbackData); + if (FAILED(hr)) + { + WcaLogError(hr, "Failed to read rollback script into CustomAction data, continuing anyway."); + } + else + { + WcaLog(LOGMSG_TRACEONLY, "Rollback Data: %ls", pwzRollbackData); + + pwz = pwzRollbackData; + hr = WcaReadStringFromCaData(&pwz, &pwzOriginalComment); + if (FAILED(hr)) + { + WcaLogError(hr, "failed to read comment from rollback data, continuing anyway"); + } + else + { + pwzComment = pwzOriginalComment; + } + hr = WcaReadIntegerFromCaData(&pwz, &iOriginalAttributes); + if (FAILED(hr)) + { + WcaLogError(hr, "failed to read attributes from rollback data, continuing anyway"); + } + else + { + iAttributes |= iOriginalAttributes; + } + } + } + + hr = RemoveGroupInternal(pwz, pwzDomain, pwzName, iAttributes); + +LExit: + WcaCaScriptClose(hRollbackScript, WCA_CASCRIPT_CLOSE_DELETE); + + ReleaseStr(pwzData); + ReleaseStr(pwzName); + ReleaseStr(pwzDomain); + ReleaseStr(pwzComment); + ReleaseStr(pwzScriptKey); + ReleaseStr(pwzRollbackData); + ReleaseStr(pwzOriginalComment); + + if (fInitializedCom) + { + ::CoUninitialize(); + } + + if (FAILED(hr)) + { + er = ERROR_INSTALL_FAILURE; + } + + return WcaFinalize(er); +} + + +/******************************************************************** + RemoveGroup - CUSTOM ACTION ENTRY POINT for removing groups + + Input: deferred CustomActionData - Name\tDomain + * *****************************************************************/ +extern "C" UINT __stdcall RemoveGroup( + MSIHANDLE hInstall +) +{ + //AssertSz(0, "Debug RemoveGroup"); + + HRESULT hr = S_OK; + UINT er = ERROR_SUCCESS; + + LPWSTR pwzData = NULL; + LPWSTR pwz = NULL; + LPWSTR pwzName = NULL; + LPWSTR pwzDomain = NULL; + LPWSTR pwzComment = NULL; + int iAttributes = 0; + BOOL fInitializedCom = FALSE; + + hr = WcaInitialize(hInstall, "RemoveGroup"); + ExitOnFailure(hr, "failed to initialize"); + + hr = ::CoInitialize(NULL); + ExitOnFailure(hr, "failed to initialize COM"); + fInitializedCom = TRUE; + + hr = WcaGetProperty(L"CustomActionData", &pwzData); + ExitOnFailure(hr, "failed to get CustomActionData"); + + WcaLog(LOGMSG_TRACEONLY, "CustomActionData: %ls", pwzData); + + // + // Read in the CustomActionData + // + pwz = pwzData; + hr = WcaReadStringFromCaData(&pwz, &pwzName); + ExitOnFailure(hr, "failed to read name from custom action data"); + + hr = WcaReadStringFromCaData(&pwz, &pwzDomain); + ExitOnFailure(hr, "failed to read domain from custom action data"); + + hr = WcaReadStringFromCaData(&pwz, &pwzComment); + ExitOnFailure(hr, "failed to read comment from custom action data"); + + hr = WcaReadIntegerFromCaData(&pwz, &iAttributes); + ExitOnFailure(hr, "failed to read attributes from custom action data"); + + hr = RemoveGroupInternal(pwz, pwzDomain, pwzName, iAttributes); + +LExit: + ReleaseStr(pwzData); + ReleaseStr(pwzName); + ReleaseStr(pwzDomain); + ReleaseStr(pwzComment); + + if (fInitializedCom) + { + ::CoUninitialize(); + } + + if (FAILED(hr)) + { + er = ERROR_INSTALL_FAILURE; + } + + return WcaFinalize(er); +} diff --git a/src/ext/Util/ca/scagroup.cpp b/src/ext/Util/ca/scagroup.cpp new file mode 100644 index 000000000..c484c1d2c --- /dev/null +++ b/src/ext/Util/ca/scagroup.cpp @@ -0,0 +1,503 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +#include "precomp.h" +#include "scanet.h" + +LPCWSTR vcsGroupQuery = L"SELECT `Group`, `Component_`, `Name`, `Domain` FROM `Wix4Group` WHERE `Group`=?"; +enum eGroupQuery { vgqGroup = 1, vgqComponent, vgqName, vgqDomain }; + +LPCWSTR vcsGroupGroupQuery = L"SELECT `Parent_`, `Child_` FROM `Wix6GroupGroup` WHERE `Child_`=?"; +enum eGroupGroupQuery { vggqParent = 1, vggqChild }; + +LPCWSTR vActionableGroupQuery = L"SELECT `Group`,`Component_`,`Name`,`Domain`,`Comment`,`Attributes` FROM `Wix4Group`,`Wix6Group` WHERE `Component_` IS NOT NULL AND `Group`=`Group_`"; +enum eActionableGroupQuery { vagqGroup = 1, vagqComponent, vagqName, vagqDomain, vagqComment, vagqAttributes }; + +static HRESULT AddGroupToList( + __inout SCA_GROUP** ppsgList + ); + + +HRESULT __stdcall ScaGetGroup( + __in LPCWSTR wzGroup, + __out SCA_GROUP* pscag + ) +{ + if (!wzGroup || *wzGroup==0 || !pscag) + { + return E_INVALIDARG; + } + + HRESULT hr = S_OK; + PMSIHANDLE hView, hRec; + + LPWSTR pwzData = NULL; + + hRec = ::MsiCreateRecord(1); + hr = WcaSetRecordString(hRec, 1, wzGroup); + ExitOnFailure(hr, "Failed to look up Group"); + + hr = WcaOpenView(vcsGroupQuery, &hView); + ExitOnFailure(hr, "Failed to open view on Wix4Group table"); + hr = WcaExecuteView(hView, hRec); + ExitOnFailure(hr, "Failed to execute view on Wix4Group table"); + + hr = WcaFetchSingleRecord(hView, &hRec); + if (S_OK == hr) + { + hr = WcaGetRecordString(hRec, vgqGroup, &pwzData); + ExitOnFailure(hr, "Failed to get Wix4Group.Group"); + hr = ::StringCchCopyW(pscag->wzKey, countof(pscag->wzKey), pwzData); + ExitOnFailure(hr, "Failed to copy key string to group object"); + + hr = WcaGetRecordString(hRec, vgqComponent, &pwzData); + ExitOnFailure(hr, "Failed to get Wix4Group.Component_"); + hr = ::StringCchCopyW(pscag->wzComponent, countof(pscag->wzComponent), pwzData); + ExitOnFailure(hr, "Failed to copy component string to group object"); + + hr = WcaGetRecordFormattedString(hRec, vgqName, &pwzData); + ExitOnFailure(hr, "Failed to get Wix4Group.Name"); + hr = ::StringCchCopyW(pscag->wzName, countof(pscag->wzName), pwzData); + ExitOnFailure(hr, "Failed to copy name string to group object"); + + hr = WcaGetRecordFormattedString(hRec, vgqDomain, &pwzData); + ExitOnFailure(hr, "Failed to get Wix4Group.Domain"); + hr = ::StringCchCopyW(pscag->wzDomain, countof(pscag->wzDomain), pwzData); + ExitOnFailure(hr, "Failed to copy domain string to group object"); + } + else if (E_NOMOREITEMS == hr) + { + WcaLog(LOGMSG_STANDARD, "Error: Cannot locate Wix4Group.Group='%ls'", wzGroup); + hr = E_FAIL; + } + else + { + ExitOnFailure(hr, "Error or found multiple matching Wix4Group rows"); + } + +LExit: + ReleaseStr(pwzData); + + return hr; +} + +HRESULT __stdcall ScaGetGroupDeferred( + __in LPCWSTR wzGroup, + __in WCA_WRAPQUERY_HANDLE hGroupQuery, + __out SCA_USER* pscag + ) +{ + if (!wzGroup || !pscag) + { + return E_INVALIDARG; + } + + HRESULT hr = S_OK; + MSIHANDLE hRec, hRecTest; + + LPWSTR pwzData = NULL; + + // clear struct and bail right away if no group key was passed to search for + ::ZeroMemory(pscag, sizeof(*pscag)); + if (!*wzGroup) + { + ExitFunction1(hr = S_OK); + } + + // Reset back to the first record + WcaFetchWrappedReset(hGroupQuery); + + hr = WcaFetchWrappedRecordWhereString(hGroupQuery, vgqGroup, wzGroup, &hRec); + if (S_OK == hr) + { + hr = WcaFetchWrappedRecordWhereString(hGroupQuery, vgqGroup, wzGroup, &hRecTest); + if (S_OK == hr) + { + AssertSz(FALSE, "Found multiple matching Wix4Group rows"); + } + + hr = WcaGetRecordString(hRec, vgqGroup, &pwzData); + ExitOnFailure(hr, "Failed to get Wix4Group.Group"); + hr = ::StringCchCopyW(pscag->wzKey, countof(pscag->wzKey), pwzData); + ExitOnFailure(hr, "Failed to copy key string to group object (in deferred CA)"); + + hr = WcaGetRecordString(hRec, vgqComponent, &pwzData); + ExitOnFailure(hr, "Failed to get Wix4Group.Component_"); + hr = ::StringCchCopyW(pscag->wzComponent, countof(pscag->wzComponent), pwzData); + ExitOnFailure(hr, "Failed to copy component string to group object (in deferred CA)"); + + hr = WcaGetRecordString(hRec, vgqName, &pwzData); + ExitOnFailure(hr, "Failed to get Wix4Group.Name"); + hr = ::StringCchCopyW(pscag->wzName, countof(pscag->wzName), pwzData); + ExitOnFailure(hr, "Failed to copy name string to group object (in deferred CA)"); + + hr = WcaGetRecordString(hRec, vgqDomain, &pwzData); + ExitOnFailure(hr, "Failed to get Wix4Group.Domain"); + hr = ::StringCchCopyW(pscag->wzDomain, countof(pscag->wzDomain), pwzData); + ExitOnFailure(hr, "Failed to copy domain string to group object (in deferred CA)"); + } + else if (E_NOMOREITEMS == hr) + { + WcaLog(LOGMSG_STANDARD, "Error: Cannot locate Wix4Group.Group='%ls'", wzGroup); + hr = E_FAIL; + } + else + { + ExitOnFailure(hr, "Error fetching single Wix4Group row"); + } + +LExit: + ReleaseStr(pwzData); + + return hr; +} + +void ScaGroupFreeList( + __in SCA_GROUP* psgList + ) +{ + SCA_GROUP* psgDelete = psgList; + while (psgList) + { + psgDelete = psgList; + psgList = psgList->psgNext; + + MemFree(psgDelete); + } +} + + +HRESULT ScaGroupRead( + __out SCA_GROUP** ppsgList + ) +{ + //Assert(FALSE); + Assert(ppsgList); + + HRESULT hr = S_OK; + UINT er = ERROR_SUCCESS; + PMSIHANDLE hView, hRec, hGroupRec, hGroupGroupView; + + LPWSTR pwzData = NULL; + + BOOL fGroupGroupExists = FALSE; + + SCA_GROUP *psg = NULL; + + INSTALLSTATE isInstalled, isAction; + + if (S_OK != WcaTableExists(L"Wix4Group")) + { + WcaLog(LOGMSG_VERBOSE, "Wix4Group Table does not exist, exiting"); + ExitFunction1(hr = S_FALSE); + } + if (S_OK != WcaTableExists(L"Wix6Group")) + { + WcaLog(LOGMSG_VERBOSE, "Wix6Group Table does not exist, exiting"); + ExitFunction1(hr = S_FALSE); + } + + if (S_OK == WcaTableExists(L"Wix6GroupGroup")) + { + fGroupGroupExists = TRUE; + } + + // + // loop through all the groups + // + hr = WcaOpenExecuteView(vActionableGroupQuery, &hView); + ExitOnFailure(hr, "failed to open view on Wix4Group,Wix6Group table(s)"); + while (S_OK == (hr = WcaFetchRecord(hView, &hRec))) + { + hr = WcaGetRecordString(hRec, vagqComponent, &pwzData); + ExitOnFailure(hr, "failed to get Wix4Group.Component"); + + er = ::MsiGetComponentStateW(WcaGetInstallHandle(), pwzData, &isInstalled, &isAction); + hr = HRESULT_FROM_WIN32(er); + ExitOnFailure(hr, "failed to get Component state for Wix4Group"); + + // don't bother if we aren't installing or uninstalling this component + if (WcaIsInstalling(isInstalled, isAction) || WcaIsUninstalling(isInstalled, isAction)) + { + // + // Add the group to the list and populate it's values + // + hr = AddGroupToList(ppsgList); + ExitOnFailure(hr, "failed to add group to list"); + + psg = *ppsgList; + + psg->isInstalled = isInstalled; + psg->isAction = isAction; + hr = ::StringCchCopyW(psg->wzComponent, countof(psg->wzComponent), pwzData); + ExitOnFailure(hr, "failed to copy component name: %ls", pwzData); + + hr = WcaGetRecordString(hRec, vagqGroup, &pwzData); + ExitOnFailure(hr, "failed to get Wix4Group.Group"); + hr = ::StringCchCopyW(psg->wzKey, countof(psg->wzKey), pwzData); + ExitOnFailure(hr, "failed to copy group key: %ls", pwzData); + + hr = WcaGetRecordFormattedString(hRec, vagqName, &pwzData); + ExitOnFailure(hr, "failed to get Wix4Group.Name"); + hr = ::StringCchCopyW(psg->wzName, countof(psg->wzName), pwzData); + ExitOnFailure(hr, "failed to copy group name: %ls", pwzData); + + hr = WcaGetRecordFormattedString(hRec, vagqDomain, &pwzData); + ExitOnFailure(hr, "failed to get Wix4Group.Domain"); + hr = ::StringCchCopyW(psg->wzDomain, countof(psg->wzDomain), pwzData); + ExitOnFailure(hr, "failed to copy group domain: %ls", pwzData); + hr = WcaGetRecordFormattedString(hRec, vagqComment, &pwzData); + ExitOnFailure(hr, "failed to get Wix6Group.Comment"); + hr = ::StringCchCopyW(psg->wzComment, countof(psg->wzComment), pwzData); + ExitOnFailure(hr, "failed to copy group comment: %ls", pwzData); + + hr = WcaGetRecordInteger(hRec, vagqAttributes, &psg->iAttributes); + ExitOnFailure(hr, "failed to get Wix6Group.Attributes"); + + // Check if this group is to be added to any other groups + if (fGroupGroupExists) + { + hGroupRec = ::MsiCreateRecord(1); + hr = WcaSetRecordString(hGroupRec, 1, psg->wzKey); + ExitOnFailure(hr, "Failed to create group record for querying Wix6GroupGroup table"); + + hr = WcaOpenExecuteView(vcsGroupGroupQuery, &hGroupGroupView); + ExitOnFailure(hr, "Failed to open view on Wix6GroupGroup table for group %ls", psg->wzKey);/* + hr = WcaExecuteView(hGroupGroupView, hGroupRec); + ExitOnFailure(hr, "Failed to execute view on Wix6GroupGroup table for group: %ls", psg->wzKey);*/ + + while (S_OK == (hr = WcaFetchRecord(hGroupGroupView, &hRec))) + { + hr = WcaGetRecordString(hRec, vggqParent, &pwzData); + ExitOnFailure(hr, "failed to get Wix6GroupGroup.Parent"); + + hr = AddGroupToList(&(psg->psgGroups)); + ExitOnFailure(hr, "failed to add group to list"); + + hr = ScaGetGroup(pwzData, psg->psgGroups); + ExitOnFailure(hr, "failed to get information for group: %ls", pwzData); + } + + if (E_NOMOREITEMS == hr) + { + hr = S_OK; + } + ExitOnFailure(hr, "failed to enumerate selected rows from Wix4UserGroup table"); + } + } + } + + if (E_NOMOREITEMS == hr) + { + hr = S_OK; + } + ExitOnFailure(hr, "failed to enumerate selected rows from Wix4Group table"); + +LExit: + ReleaseStr(pwzData); + + return hr; +} + +/* **************************************************************** +ScaGroupExecute - Schedules group account creation or removal based on +component state. + +******************************************************************/ +HRESULT ScaGroupExecute( + __in SCA_GROUP *psgList + ) +{ + HRESULT hr = S_OK; + DWORD er = 0; + + LPWSTR pwzBaseScriptKey = NULL; + DWORD cScriptKey = 0; + + LOCALGROUP_INFO_0 *pGroupInfo = NULL; + LPWSTR pwzScriptKey = NULL; + LPWSTR pwzActionData = NULL; + LPWSTR pwzRollbackData = NULL; + LPWSTR pwzServerName = NULL; + + // Get the base script key for this CustomAction. + hr = WcaCaScriptCreateKey(&pwzBaseScriptKey); + ExitOnFailure(hr, "Failed to get encoding key."); + + // Loop through all the users to be configured. + for (SCA_GROUP *psg = psgList; psg; psg = psg->psgNext) + { + GROUP_EXISTS geGroupExists = GROUP_EXISTS_INDETERMINATE; + + // Always put the Group Name, Domain, and Comment on the front of the CustomAction data. + // The attributes will be added when we have finished adjusting them. Sometimes we'll + // add more data. + Assert(psg->wzName); + hr = WcaWriteStringToCaData(psg->wzName, &pwzActionData); + ExitOnFailure(hr, "Failed to add group name to custom action data: %ls", psg->wzName); + hr = WcaWriteStringToCaData(psg->wzDomain, &pwzActionData); + ExitOnFailure(hr, "Failed to add group domain to custom action data: %ls", psg->wzDomain); + hr = WcaWriteStringToCaData(psg->wzComment, &pwzActionData); + ExitOnFailure(hr, "Failed to add group comment to custom action data: %ls", psg->wzComment); + + // Check to see if the group already exists since we have to be very careful when adding + // and removing groups. Note: MSDN says that it is safe to call these APIs from any + // user, so we should be safe calling it during immediate mode. + + LPCWSTR wzDomain = psg->wzDomain; + hr = GetDomainServerName(wzDomain, &pwzServerName); + + er = ::NetLocalGroupGetInfo(pwzServerName, psg->wzName, 0, reinterpret_cast(&pGroupInfo)); + if (NERR_Success == er) + { + geGroupExists = GROUP_EXISTS_YES; + } + else if (NERR_GroupNotFound == er) + { + geGroupExists = GROUP_EXISTS_NO; + } + else + { + geGroupExists = GROUP_EXISTS_INDETERMINATE; + hr = HRESULT_FROM_WIN32(er); + WcaLog(LOGMSG_VERBOSE, "Failed to check existence of domain: %ls, group: %ls (error code 0x%x) - continuing", wzDomain, psg->wzName, hr); + hr = S_OK; + er = ERROR_SUCCESS; + } + + if (WcaIsInstalling(psg->isInstalled, psg->isAction)) + { + // If the group exists, check to see if we are supposed to fail if the group exists before + // the install. + if (GROUP_EXISTS_YES == geGroupExists) + { + // Re-installs will always fail if we don't remove the check for "fail if exists". + if (WcaIsReInstalling(psg->isInstalled, psg->isAction)) + { + psg->iAttributes &= ~SCAG_FAIL_IF_EXISTS; + + // If install would create the group, re-install should be able to update the group. + if (!(psg->iAttributes & SCAG_DONT_CREATE_GROUP)) + { + psg->iAttributes |= SCAG_UPDATE_IF_EXISTS; + } + } + + if (SCAG_FAIL_IF_EXISTS & psg->iAttributes + && !(SCAG_UPDATE_IF_EXISTS & psg->iAttributes)) + { + hr = HRESULT_FROM_WIN32(NERR_GroupExists); + MessageExitOnFailure(hr, msierrGRPFailedGroupCreateExists, "Failed to create group: %ls because group already exists.", psg->wzName); + } + } + + hr = WcaWriteIntegerToCaData(psg->iAttributes, &pwzActionData); + ExitOnFailure(hr, "failed to add group attributes to custom action data for group: %ls", psg->wzKey); + + // Rollback only if the group already exists, we couldn't determine if the group exists, or we are going to create the group + if ((GROUP_EXISTS_YES == geGroupExists) + || (GROUP_EXISTS_INDETERMINATE == geGroupExists) + || !(psg->iAttributes & SCAG_DONT_CREATE_GROUP)) + { + ++cScriptKey; + hr = StrAllocFormatted(&pwzScriptKey, L"%ls%u", pwzBaseScriptKey, cScriptKey); + ExitOnFailure(hr, "Failed to create encoding key."); + + // Write the script key to CustomActionData for install and rollback so information can be passed to rollback. + hr = WcaWriteStringToCaData(pwzScriptKey, &pwzActionData); + ExitOnFailure(hr, "Failed to add encoding key to custom action data."); + + hr = WcaWriteStringToCaData(pwzScriptKey, &pwzRollbackData); + ExitOnFailure(hr, "Failed to add encoding key to rollback custom action data."); + + INT iRollbackUserAttributes = psg->iAttributes; + + // If the user already exists, ensure this is accounted for in rollback + if (GROUP_EXISTS_YES == geGroupExists) + { + iRollbackUserAttributes |= SCAG_DONT_CREATE_GROUP; + } + else + { + iRollbackUserAttributes &= ~SCAG_DONT_CREATE_GROUP; + } + + hr = WcaWriteStringToCaData(psg->wzName, &pwzRollbackData); + ExitOnFailure(hr, "Failed to add group name to rollback custom action data: %ls", psg->wzName); + hr = WcaWriteStringToCaData(psg->wzDomain, &pwzRollbackData); + ExitOnFailure(hr, "Failed to add group domain to rollback custom action data: %ls", psg->wzDomain); + hr = WcaWriteIntegerToCaData(iRollbackUserAttributes, &pwzRollbackData); + ExitOnFailure(hr, "failed to add group attributes to rollback custom action data for group: %ls", psg->wzKey); + + hr = WcaDoDeferredAction(CUSTOM_ACTION_DECORATION(L"CreateGroupRollback"), pwzRollbackData, COST_GROUP_DELETE); + ExitOnFailure(hr, "failed to schedule CreateGroupRollback"); + } + else + { + // Write empty script key to CustomActionData since there is no rollback. + hr = WcaWriteStringToCaData(L"", &pwzActionData); + ExitOnFailure(hr, "Failed to add empty encoding key to custom action data."); + } + + // + // Schedule the creation now. + // + hr = WcaDoDeferredAction(CUSTOM_ACTION_DECORATION(L"CreateGroup"), pwzActionData, COST_GROUP_ADD); + ExitOnFailure(hr, "failed to schedule CreateGroup"); + } + else if (((GROUP_EXISTS_YES == geGroupExists) + || (GROUP_EXISTS_INDETERMINATE == geGroupExists)) + && WcaIsUninstalling(psg->isInstalled, psg->isAction) + && !(psg->iAttributes & SCAG_DONT_REMOVE_ON_UNINSTALL)) + { + hr = WcaWriteIntegerToCaData(psg->iAttributes, &pwzActionData); + ExitOnFailure(hr, "failed to add group attributes to custom action data for group: %ls", psg->wzKey); + + // Schedule the removal because the group exists and we don't have any flags set + // that say not to remove the group on uninstall. + // + // Note: We can't rollback the removal of a group which is why RemoveGroup is a commit + // CustomAction. + hr = WcaDoDeferredAction(CUSTOM_ACTION_DECORATION(L"RemoveGroup"), pwzActionData, COST_GROUP_DELETE); + ExitOnFailure(hr, "failed to schedule RemoveGroup"); + } + + ReleaseNullStr(pwzScriptKey); + ReleaseNullStr(pwzActionData); + ReleaseNullStr(pwzRollbackData); + ReleaseNullStr(pwzServerName); + if (pGroupInfo) + { + ::NetApiBufferFree(static_cast(pGroupInfo)); + pGroupInfo = NULL; + } + } + +LExit: + ReleaseStr(pwzBaseScriptKey); + ReleaseStr(pwzScriptKey); + ReleaseStr(pwzActionData); + ReleaseStr(pwzRollbackData); + ReleaseStr(pwzServerName); + if (pGroupInfo) + { + ::NetApiBufferFree(static_cast(pGroupInfo)); + pGroupInfo = NULL; + } + + return hr; +} + +static HRESULT AddGroupToList( + __inout SCA_GROUP** ppsgList + ) +{ + HRESULT hr = S_OK; + SCA_GROUP* psg = static_cast(MemAlloc(sizeof(SCA_GROUP), TRUE)); + ExitOnNull(psg, hr, E_OUTOFMEMORY, "failed to allocate memory for new group list element"); + + psg->psgNext = *ppsgList; + *ppsgList = psg; + +LExit: + return hr; +} diff --git a/src/ext/Util/ca/scagroup.h b/src/ext/Util/ca/scagroup.h new file mode 100644 index 000000000..8666d8521 --- /dev/null +++ b/src/ext/Util/ca/scagroup.h @@ -0,0 +1,47 @@ +#pragma once +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +enum GROUP_EXISTS +{ + GROUP_EXISTS_YES, + GROUP_EXISTS_NO, + GROUP_EXISTS_INDETERMINATE +}; + +// structs +struct SCA_GROUP +{ + WCHAR wzKey[MAX_DARWIN_KEY + 1]; + WCHAR wzComponent[MAX_DARWIN_KEY + 1]; + INSTALLSTATE isInstalled; + INSTALLSTATE isAction; + + WCHAR wzDomain[MAX_DARWIN_COLUMN + 1]; + WCHAR wzName[MAX_DARWIN_COLUMN + 1]; + WCHAR wzComment[MAX_DARWIN_COLUMN + 1]; + INT iAttributes; + + SCA_GROUP* psgGroups; + + SCA_GROUP *psgNext; +}; + +// prototypes +HRESULT __stdcall ScaGetGroup( + __in LPCWSTR wzGroup, + __out SCA_GROUP* pscag + ); +HRESULT __stdcall ScaGetGroupDeferred( + __in LPCWSTR wzGroup, + __in WCA_WRAPQUERY_HANDLE hGroupQuery, + __out SCA_GROUP* pscag + ); +void ScaGroupFreeList( + __in SCA_GROUP* psgList + ); +HRESULT ScaGroupRead( + __inout SCA_GROUP** ppsgList + ); +HRESULT ScaGroupExecute( + __in SCA_GROUP*psgList + ); diff --git a/src/ext/Util/ca/scanet.cpp b/src/ext/Util/ca/scanet.cpp new file mode 100644 index 000000000..11ee487de --- /dev/null +++ b/src/ext/Util/ca/scanet.cpp @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +#include "precomp.h" +#include "scanet.h" + + +HRESULT GetDomainServerName(LPCWSTR pwzDomain, LPWSTR* ppwzServerName, ULONG flags) +{ + DWORD er = ERROR_SUCCESS; + PDOMAIN_CONTROLLER_INFOW pDomainControllerInfo = NULL; + HRESULT hr = S_OK; + + if (pwzDomain && *pwzDomain) + { + er = ::DsGetDcNameW(NULL, pwzDomain, NULL, NULL, flags, &pDomainControllerInfo); + if (RPC_S_SERVER_UNAVAILABLE == er) + { + // MSDN says, if we get the above error code, try again with the "DS_FORCE_REDISCOVERY" flag + er = ::DsGetDcNameW(NULL, pwzDomain, NULL, NULL, flags | DS_FORCE_REDISCOVERY, &pDomainControllerInfo); + } + + if (ERROR_SUCCESS == er && pDomainControllerInfo->DomainControllerName) + { + // Skip the \\ prefix if present. + if ('\\' == *pDomainControllerInfo->DomainControllerName && '\\' == *pDomainControllerInfo->DomainControllerName + 1) + { + hr = StrAllocString(ppwzServerName, pDomainControllerInfo->DomainControllerName + 2, 0); + ExitOnFailure(hr, "failed to allocate memory for string"); + } + else + { + hr = StrAllocString(ppwzServerName, pDomainControllerInfo->DomainControllerName, 0); + ExitOnFailure(hr, "failed to allocate memory for string"); + } + } + else + { + StrAllocString(ppwzServerName, pwzDomain, 0); + hr = HRESULT_FROM_WIN32(er); + ExitOnFailure(hr, "failed to contact domain %ls", pwzDomain); + } + } + +LExit: + if (pDomainControllerInfo) + { + ::NetApiBufferFree((LPVOID)pDomainControllerInfo); + } + return hr; +} diff --git a/src/ext/Util/ca/scanet.h b/src/ext/Util/ca/scanet.h new file mode 100644 index 000000000..1fee61f82 --- /dev/null +++ b/src/ext/Util/ca/scanet.h @@ -0,0 +1,4 @@ +#pragma once +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +HRESULT GetDomainServerName(LPCWSTR pwzDomain, LPWSTR* ppwzServerName, ULONG flags = 0); diff --git a/src/ext/Util/ca/scasched.cpp b/src/ext/Util/ca/scasched.cpp index d81b1f141..1351fbfde 100644 --- a/src/ext/Util/ca/scasched.cpp +++ b/src/ext/Util/ca/scasched.cpp @@ -124,4 +124,49 @@ extern "C" UINT __stdcall ConfigureUsers( er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; return WcaFinalize(er); -} \ No newline at end of file +} + +/******************************************************************** +ConfigureGroups - CUSTOM ACTION ENTRY POINT for installing groups + +********************************************************************/ +extern "C" UINT __stdcall ConfigureGroups( + __in MSIHANDLE hInstall +) +{ + //AssertSz(0, "Debug ConfigureGroups"); + + HRESULT hr = S_OK; + UINT er = ERROR_SUCCESS; + + BOOL fInitializedCom = FALSE; + SCA_GROUP* psgList = NULL; + + // initialize + hr = WcaInitialize(hInstall, "ConfigureGroups"); + ExitOnFailure(hr, "Failed to initialize"); + + hr = ::CoInitialize(NULL); + ExitOnFailure(hr, "failed to initialize COM"); + fInitializedCom = TRUE; + + hr = ScaGroupRead(&psgList); + ExitOnFailure(hr, "failed to read Wix4Group,Wix6Group table(s)"); + + hr = ScaGroupExecute(psgList); + ExitOnFailure(hr, "failed to add/remove Group actions"); + +LExit: + if (psgList) + { + ScaGroupFreeList(psgList); + } + + if (fInitializedCom) + { + ::CoUninitialize(); + } + + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} diff --git a/src/ext/Util/ca/scauser.cpp b/src/ext/Util/ca/scauser.cpp index b643a8429..ecfe43bef 100644 --- a/src/ext/Util/ca/scauser.cpp +++ b/src/ext/Util/ca/scauser.cpp @@ -5,9 +5,6 @@ LPCWSTR vcsUserQuery = L"SELECT `User`, `Component_`, `Name`, `Domain`, `Comment`, `Password` FROM `Wix4User` WHERE `User`=?"; enum eUserQuery { vuqUser = 1, vuqComponent, vuqName, vuqDomain, vuqComment, vuqPassword }; -LPCWSTR vcsGroupQuery = L"SELECT `Group`, `Component_`, `Name`, `Domain` FROM `Wix4Group` WHERE `Group`=?"; -enum eGroupQuery { vgqGroup = 1, vgqComponent, vgqName, vgqDomain }; - LPCWSTR vcsUserGroupQuery = L"SELECT `User_`, `Group_` FROM `Wix4UserGroup` WHERE `User_`=?"; enum eUserGroupQuery { vugqUser = 1, vugqGroup }; @@ -185,71 +182,6 @@ HRESULT __stdcall ScaGetUserDeferred( return hr; } - -HRESULT __stdcall ScaGetGroup( - __in LPCWSTR wzGroup, - __out SCA_GROUP* pscag - ) -{ - if (!wzGroup || !pscag) - { - return E_INVALIDARG; - } - - HRESULT hr = S_OK; - PMSIHANDLE hView, hRec; - - LPWSTR pwzData = NULL; - - hRec = ::MsiCreateRecord(1); - hr = WcaSetRecordString(hRec, 1, wzGroup); - ExitOnFailure(hr, "Failed to look up Group"); - - hr = WcaOpenView(vcsGroupQuery, &hView); - ExitOnFailure(hr, "Failed to open view on Wix4Group table"); - hr = WcaExecuteView(hView, hRec); - ExitOnFailure(hr, "Failed to execute view on Wix4Group table"); - - hr = WcaFetchSingleRecord(hView, &hRec); - if (S_OK == hr) - { - hr = WcaGetRecordString(hRec, vgqGroup, &pwzData); - ExitOnFailure(hr, "Failed to get Wix4Group.Group."); - hr = ::StringCchCopyW(pscag->wzKey, countof(pscag->wzKey), pwzData); - ExitOnFailure(hr, "Failed to copy Wix4Group.Group."); - - hr = WcaGetRecordString(hRec, vgqComponent, &pwzData); - ExitOnFailure(hr, "Failed to get Wix4Group.Component_"); - hr = ::StringCchCopyW(pscag->wzComponent, countof(pscag->wzComponent), pwzData); - ExitOnFailure(hr, "Failed to copy Wix4Group.Component_."); - - hr = WcaGetRecordFormattedString(hRec, vgqName, &pwzData); - ExitOnFailure(hr, "Failed to get Wix4Group.Name"); - hr = ::StringCchCopyW(pscag->wzName, countof(pscag->wzName), pwzData); - ExitOnFailure(hr, "Failed to copy Wix4Group.Name."); - - hr = WcaGetRecordFormattedString(hRec, vgqDomain, &pwzData); - ExitOnFailure(hr, "Failed to get Wix4Group.Domain"); - hr = ::StringCchCopyW(pscag->wzDomain, countof(pscag->wzDomain), pwzData); - ExitOnFailure(hr, "Failed to copy Wix4Group.Domain."); - } - else if (E_NOMOREITEMS == hr) - { - WcaLog(LOGMSG_STANDARD, "Error: Cannot locate Wix4Group.Group='%ls'", wzGroup); - hr = E_FAIL; - } - else - { - ExitOnFailure(hr, "Error or found multiple matching Wix4Group rows"); - } - -LExit: - ReleaseStr(pwzData); - - return hr; -} - - void ScaUserFreeList( __in SCA_USER* psuList ) @@ -266,21 +198,6 @@ void ScaUserFreeList( } -void ScaGroupFreeList( - __in SCA_GROUP* psgList - ) -{ - SCA_GROUP* psgDelete = psgList; - while (psgList) - { - psgDelete = psgList; - psgList = psgList->psgNext; - - MemFree(psgDelete); - } -} - - HRESULT ScaUserRead( __out SCA_USER** ppsuList ) diff --git a/src/ext/Util/ca/scauser.h b/src/ext/Util/ca/scauser.h index 3da847b50..de6900867 100644 --- a/src/ext/Util/ca/scauser.h +++ b/src/ext/Util/ca/scauser.h @@ -1,6 +1,6 @@ #pragma once // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. - +#include "scagroup.h" enum USER_EXISTS { @@ -9,17 +9,6 @@ enum USER_EXISTS USER_EXISTS_INDETERMINATE }; -// structs -struct SCA_GROUP -{ - WCHAR wzKey[MAX_DARWIN_KEY + 1]; - WCHAR wzComponent[MAX_DARWIN_KEY + 1]; - - WCHAR wzDomain[MAX_DARWIN_COLUMN + 1]; - WCHAR wzName[MAX_DARWIN_COLUMN + 1]; - - SCA_GROUP *psgNext; -}; struct SCA_USER { @@ -50,16 +39,9 @@ HRESULT __stdcall ScaGetUserDeferred( __in WCA_WRAPQUERY_HANDLE hUserQuery, __out SCA_USER* pscau ); -HRESULT __stdcall ScaGetGroup( - __in LPCWSTR wzGroup, - __out SCA_GROUP* pscag - ); void ScaUserFreeList( __in SCA_USER* psuList ); -void ScaGroupFreeList( - __in SCA_GROUP* psgList - ); HRESULT ScaUserRead( __inout SCA_USER** ppsuList ); diff --git a/src/ext/Util/ca/utilca.def b/src/ext/Util/ca/utilca.def index 96545566a..18a19d12e 100644 --- a/src/ext/Util/ca/utilca.def +++ b/src/ext/Util/ca/utilca.def @@ -43,6 +43,9 @@ EXPORTS UnregisterPerfmon CreateSmb DropSmb + CreateGroup + CreateGroupRollback + RemoveGroup CreateUser CreateUserRollback RemoveUser @@ -51,6 +54,7 @@ EXPORTS ConfigurePerfmonUninstall ConfigureSmbInstall ConfigureSmbUninstall + ConfigureGroups ConfigureUsers InstallPerfCounterData UninstallPerfCounterData diff --git a/src/ext/Util/ca/utilca.vcxproj b/src/ext/Util/ca/utilca.vcxproj index 758f075c2..5dbe2792c 100644 --- a/src/ext/Util/ca/utilca.vcxproj +++ b/src/ext/Util/ca/utilca.vcxproj @@ -61,7 +61,9 @@ + + @@ -84,6 +86,8 @@ + + diff --git a/src/ext/Util/test/WixToolsetTest.Util/TestData/CreateGroup/Package.wxs b/src/ext/Util/test/WixToolsetTest.Util/TestData/CreateGroup/Package.wxs new file mode 100644 index 000000000..fdbbb9cc8 --- /dev/null +++ b/src/ext/Util/test/WixToolsetTest.Util/TestData/CreateGroup/Package.wxs @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/ext/Util/test/WixToolsetTest.Util/TestData/CreateGroup/PackageComponents.wxs b/src/ext/Util/test/WixToolsetTest.Util/TestData/CreateGroup/PackageComponents.wxs new file mode 100644 index 000000000..ce9ab4182 --- /dev/null +++ b/src/ext/Util/test/WixToolsetTest.Util/TestData/CreateGroup/PackageComponents.wxs @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ext/Util/test/WixToolsetTest.Util/UtilExtensionFixture.cs b/src/ext/Util/test/WixToolsetTest.Util/UtilExtensionFixture.cs index 0a93f3a41..d71dd8244 100644 --- a/src/ext/Util/test/WixToolsetTest.Util/UtilExtensionFixture.cs +++ b/src/ext/Util/test/WixToolsetTest.Util/UtilExtensionFixture.cs @@ -394,6 +394,159 @@ public void CanBuildBundleWithSearches() } } + [Fact] + public void CanCreateUserGroupWithComment() + { + var folder = TestData.Get(@"TestData\CreateGroup"); + var build = new Builder(folder, typeof(UtilExtensionFactory), new[] { folder }); + + var results = build.BuildAndQuery(BuildX64, "Binary", "CustomAction", "Wix4Group", "Wix6Group"); + WixAssert.CompareLineByLine(new[] + { + "Binary:Wix4UtilCA_X64\t[Binary data]", + "CustomAction:Wix4ConfigureGroups_X64\t1\tWix4UtilCA_X64\tConfigureGroups\t", + "CustomAction:Wix4CreateGroup_X64\t11265\tWix4UtilCA_X64\tCreateGroup\t", + "CustomAction:Wix4CreateGroupRollback_X64\t11521\tWix4UtilCA_X64\tCreateGroupRollback\t", + "CustomAction:Wix4RemoveGroup_X64\t11841\tWix4UtilCA_X64\tRemoveGroup\t", + "Wix4Group:TEST_GROUP00\tComponent1\ttestName00\t", + "Wix4Group:TEST_GROUP01\tComponent1\ttestName01\t", + "Wix4Group:TEST_GROUP02\tComponent1\ttestName02\t", + "Wix4Group:TEST_GROUP03\tComponent1\ttestName03\t", + "Wix4Group:TEST_GROUP04\tComponent1\ttestName04\t", + "Wix4Group:TEST_GROUP05\tComponent1\ttestName05\t", + "Wix4Group:TEST_GROUP06\tComponent1\ttestName06\t", + "Wix4Group:TEST_GROUP07\tComponent1\ttestName07\t", + "Wix4Group:TEST_GROUP08\tComponent1\ttestName08\t", + "Wix4Group:TEST_GROUP09\tComponent1\ttestName09\t", + "Wix4Group:TEST_GROUP10\tComponent1\ttestName10\t", + "Wix4Group:TEST_GROUP11\tComponent1\ttestName11\t", + "Wix4Group:TEST_GROUP12\tComponent1\ttestName12\t", + "Wix4Group:TEST_GROUP13\tComponent1\ttestName13\t", + "Wix4Group:TEST_GROUP14\tComponent1\ttestName14\t", + "Wix4Group:TEST_GROUP15\tComponent1\ttestName15\t", + "Wix4Group:TEST_GROUP16\tComponent1\ttestName16\t", + "Wix4Group:TEST_GROUP17\tComponent1\ttestName17\t", + "Wix4Group:TEST_GROUP18\tComponent1\ttestName18\t", + "Wix4Group:TEST_GROUP19\tComponent1\ttestName19\t", + "Wix4Group:TEST_GROUP20\tComponent1\ttestName20\t", + "Wix4Group:TEST_GROUP21\tComponent1\ttestName21\t", + "Wix4Group:TEST_GROUP22\tComponent1\ttestName22\t", + "Wix4Group:TEST_GROUP23\tComponent1\ttestName23\t", + "Wix4Group:TEST_GROUP24\tComponent1\ttestName24\t", + "Wix4Group:TEST_GROUP25\tComponent1\ttestName25\t", + "Wix4Group:TEST_GROUP26\tComponent1\ttestName26\t", + "Wix4Group:TEST_GROUP27\tComponent1\ttestName27\t", + "Wix4Group:TEST_GROUP28\tComponent1\ttestName28\t", + "Wix4Group:TEST_GROUP29\tComponent1\ttestName29\t", + "Wix4Group:TEST_GROUP30\tComponent1\ttestName30\t", + "Wix4Group:TEST_GROUP31\tComponent1\ttestName31\t", + "Wix4Group:TEST_GROUP32\tComponent1\ttestName32\t", + "Wix4Group:TEST_GROUP33\tComponent1\ttestName33\t", + "Wix4Group:TEST_GROUP34\tComponent1\ttestName34\t", + "Wix4Group:TEST_GROUP35\tComponent1\ttestName35\t", + "Wix4Group:TEST_GROUP36\tComponent1\ttestName36\t", + "Wix4Group:TEST_GROUP37\tComponent1\ttestName37\t", + "Wix4Group:TEST_GROUP38\tComponent1\ttestName38\t", + "Wix4Group:TEST_GROUP39\tComponent1\ttestName39\t", + "Wix4Group:TEST_GROUP40\tComponent1\ttestName40\t", + "Wix4Group:TEST_GROUP41\tComponent1\ttestName41\t", + "Wix4Group:TEST_GROUP42\tComponent1\ttestName42\t", + "Wix4Group:TEST_GROUP43\tComponent1\ttestName43\t", + "Wix4Group:TEST_GROUP44\tComponent1\ttestName44\t", + "Wix4Group:TEST_GROUP45\tComponent1\ttestName45\t", + "Wix4Group:TEST_GROUP46\tComponent1\ttestName46\t", + "Wix4Group:TEST_GROUP47\tComponent1\ttestName47\t", + "Wix4Group:TEST_GROUP48\tComponent1\ttestName48\t", + "Wix4Group:TEST_GROUP49\tComponent1\ttestName49\t", + "Wix4Group:TEST_GROUP50\tComponent1\ttestName50\t", + "Wix4Group:TEST_GROUP51\tComponent1\ttestName51\t", + "Wix4Group:TEST_GROUP52\tComponent1\ttestName52\t", + "Wix4Group:TEST_GROUP53\tComponent1\ttestName53\t", + "Wix4Group:TEST_GROUP54\tComponent1\ttestName54\t", + "Wix4Group:TEST_GROUP55\tComponent1\ttestName55\t", + "Wix4Group:TEST_GROUP56\tComponent1\ttestName56\t", + "Wix4Group:TEST_GROUP57\tComponent1\ttestName57\t", + "Wix4Group:TEST_GROUP58\tComponent1\ttestName58\t", + "Wix4Group:TEST_GROUP59\tComponent1\ttestName59\t", + "Wix4Group:TEST_GROUP60\tComponent1\ttestName60\t", + "Wix4Group:TEST_GROUP61\tComponent1\ttestName61\t", + "Wix4Group:TEST_GROUP62\tComponent1\ttestName62\t", + "Wix4Group:TEST_GROUP63\tComponent1\ttestName63\t", + "Wix4Group:TEST_GROUP64\tComponent1\ttestName64\ttestDomain00", + "Wix4Group:TEST_GROUP65\tComponent1\ttestName65\ttestDomain01", + "Wix4Group:TEST_GROUP66\tComponent1\ttestName66\ttestDomain02", + "Wix4Group:TEST_GROUP67\tComponent1\ttestName67\ttestDomain03", + "Wix6Group:TEST_GROUP00\tTest Comment 1\t0", + "Wix6Group:TEST_GROUP01\tTest Comment 1\t1", + "Wix6Group:TEST_GROUP02\t\t2", + "Wix6Group:TEST_GROUP03\t\t3", + "Wix6Group:TEST_GROUP04\tTest Comment 1\t4", + "Wix6Group:TEST_GROUP05\tTest Comment 1\t5", + "Wix6Group:TEST_GROUP06\t\t6", + "Wix6Group:TEST_GROUP07\t\t7", + "Wix6Group:TEST_GROUP08\tTest Comment 1\t8", + "Wix6Group:TEST_GROUP09\tTest Comment 1\t9", + "Wix6Group:TEST_GROUP10\t\t10", + "Wix6Group:TEST_GROUP11\t\t11", + "Wix6Group:TEST_GROUP12\tTest Comment 1\t12", + "Wix6Group:TEST_GROUP13\tTest Comment 1\t13", + "Wix6Group:TEST_GROUP14\t\t14", + "Wix6Group:TEST_GROUP15\t\t15", + "Wix6Group:TEST_GROUP16\tTest Comment 1\t16", + "Wix6Group:TEST_GROUP17\tTest Comment 1\t17", + "Wix6Group:TEST_GROUP18\t\t18", + "Wix6Group:TEST_GROUP19\t\t19", + "Wix6Group:TEST_GROUP20\tTest Comment 1\t20", + "Wix6Group:TEST_GROUP21\tTest Comment 1\t21", + "Wix6Group:TEST_GROUP22\t\t22", + "Wix6Group:TEST_GROUP23\t\t23", + "Wix6Group:TEST_GROUP24\tTest Comment 1\t24", + "Wix6Group:TEST_GROUP25\tTest Comment 1\t25", + "Wix6Group:TEST_GROUP26\t\t26", + "Wix6Group:TEST_GROUP27\t\t27", + "Wix6Group:TEST_GROUP28\tTest Comment 1\t28", + "Wix6Group:TEST_GROUP29\tTest Comment 1\t29", + "Wix6Group:TEST_GROUP30\t\t30", + "Wix6Group:TEST_GROUP31\t\t31", + "Wix6Group:TEST_GROUP32\t\t32", + "Wix6Group:TEST_GROUP33\t\t33", + "Wix6Group:TEST_GROUP34\t\t34", + "Wix6Group:TEST_GROUP35\t\t35", + "Wix6Group:TEST_GROUP36\t\t36", + "Wix6Group:TEST_GROUP37\t\t37", + "Wix6Group:TEST_GROUP38\t\t38", + "Wix6Group:TEST_GROUP39\t\t39", + "Wix6Group:TEST_GROUP40\t\t40", + "Wix6Group:TEST_GROUP41\t\t41", + "Wix6Group:TEST_GROUP42\t\t42", + "Wix6Group:TEST_GROUP43\t\t43", + "Wix6Group:TEST_GROUP44\t\t44", + "Wix6Group:TEST_GROUP45\t\t45", + "Wix6Group:TEST_GROUP46\t\t46", + "Wix6Group:TEST_GROUP47\t\t47", + "Wix6Group:TEST_GROUP48\t\t48", + "Wix6Group:TEST_GROUP49\t\t49", + "Wix6Group:TEST_GROUP50\t\t50", + "Wix6Group:TEST_GROUP51\t\t51", + "Wix6Group:TEST_GROUP52\t\t52", + "Wix6Group:TEST_GROUP53\t\t53", + "Wix6Group:TEST_GROUP54\t\t54", + "Wix6Group:TEST_GROUP55\t\t55", + "Wix6Group:TEST_GROUP56\t\t56", + "Wix6Group:TEST_GROUP57\t\t57", + "Wix6Group:TEST_GROUP58\t\t58", + "Wix6Group:TEST_GROUP59\t\t59", + "Wix6Group:TEST_GROUP60\t\t60", + "Wix6Group:TEST_GROUP61\t\t61", + "Wix6Group:TEST_GROUP62\t\t62", + "Wix6Group:TEST_GROUP63\t\t63", + "Wix6Group:TEST_GROUP64\tTest Comment 1\t0", + "Wix6Group:TEST_GROUP65\tTest Comment 1\t1", + "Wix6Group:TEST_GROUP66\t\t2", + "Wix6Group:TEST_GROUP67\t\t3", + }, results.OrderBy(s => s).ToArray()); + } + [Fact] public void CanCreateUserAccountWithComment() { diff --git a/src/ext/Util/wixext/Symbols/GroupGroupSymbol.cs b/src/ext/Util/wixext/Symbols/GroupGroupSymbol.cs new file mode 100644 index 000000000..fdd1ee760 --- /dev/null +++ b/src/ext/Util/wixext/Symbols/GroupGroupSymbol.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Util +{ + using WixToolset.Data; + using WixToolset.Util.Symbols; + + public static partial class UtilSymbolDefinitions + { + public static readonly IntermediateSymbolDefinition GroupGroup = new IntermediateSymbolDefinition( + UtilSymbolDefinitionType.GroupGroup.ToString(), + new[] + { + new IntermediateFieldDefinition(nameof(GroupGroupSymbol.SymbolFields.ParentGroupRef), IntermediateFieldType.String), + new IntermediateFieldDefinition(nameof(GroupGroupSymbol.SymbolFields.ChildGroupRef), IntermediateFieldType.String), + }, + typeof(UserGroupSymbol)); + } +} + +namespace WixToolset.Util.Symbols +{ + using WixToolset.Data; + + public class GroupGroupSymbol : IntermediateSymbol + { + public enum SymbolFields + { + ParentGroupRef, + ChildGroupRef, + } + + public GroupGroupSymbol() : base(UtilSymbolDefinitions.GroupGroup, null, null) + { + } + + public GroupGroupSymbol(SourceLineNumber sourceLineNumber, Identifier id = null) : base(UtilSymbolDefinitions.GroupGroup, sourceLineNumber, id) + { + } + + public IntermediateField this[GroupGroupSymbol.SymbolFields index] => this.Fields[(int)index]; + + public string ParentGroupRef + { + get => this.Fields[(int)GroupGroupSymbol.SymbolFields.ParentGroupRef].AsString(); + set => this.Set((int)GroupGroupSymbol.SymbolFields.ParentGroupRef, value); + } + + public string ChildGroupRef + { + get => this.Fields[(int)GroupGroupSymbol.SymbolFields.ChildGroupRef].AsString(); + set => this.Set((int)GroupGroupSymbol.SymbolFields.ChildGroupRef, value); + } + + } +} diff --git a/src/ext/Util/wixext/Symbols/GroupSymbol.cs b/src/ext/Util/wixext/Symbols/GroupSymbol.cs index b378db442..ef1dc33fc 100644 --- a/src/ext/Util/wixext/Symbols/GroupSymbol.cs +++ b/src/ext/Util/wixext/Symbols/GroupSymbol.cs @@ -11,27 +11,38 @@ public static partial class UtilSymbolDefinitions UtilSymbolDefinitionType.Group.ToString(), new[] { - new IntermediateFieldDefinition(nameof(GroupSymbolFields.ComponentRef), IntermediateFieldType.String), - new IntermediateFieldDefinition(nameof(GroupSymbolFields.Name), IntermediateFieldType.String), - new IntermediateFieldDefinition(nameof(GroupSymbolFields.Domain), IntermediateFieldType.String), + new IntermediateFieldDefinition(nameof(GroupSymbol.SymbolFields.ComponentRef), IntermediateFieldType.String), + new IntermediateFieldDefinition(nameof(GroupSymbol.SymbolFields.Name), IntermediateFieldType.String), + new IntermediateFieldDefinition(nameof(GroupSymbol.SymbolFields.Domain), IntermediateFieldType.String), }, typeof(GroupSymbol)); + + public static readonly IntermediateSymbolDefinition Group6 = new IntermediateSymbolDefinition( + UtilSymbolDefinitionType.Group6.ToString(), + new[] + { + new IntermediateFieldDefinition(nameof(Group6Symbol.SymbolFields.GroupRef), IntermediateFieldType.String), + new IntermediateFieldDefinition(nameof(Group6Symbol.SymbolFields.Comment), IntermediateFieldType.String), + new IntermediateFieldDefinition(nameof(Group6Symbol.SymbolFields.Attributes), IntermediateFieldType.Number), + }, + typeof(Group6Symbol)); } } namespace WixToolset.Util.Symbols { + using System; using WixToolset.Data; - public enum GroupSymbolFields - { - ComponentRef, - Name, - Domain, - } - public class GroupSymbol : IntermediateSymbol { + public enum SymbolFields + { + ComponentRef, + Name, + Domain, + } + public GroupSymbol() : base(UtilSymbolDefinitions.Group, null, null) { } @@ -40,24 +51,74 @@ public GroupSymbol(SourceLineNumber sourceLineNumber, Identifier id = null) : ba { } - public IntermediateField this[GroupSymbolFields index] => this.Fields[(int)index]; + public IntermediateField this[GroupSymbol.SymbolFields index] => this.Fields[(int)index]; public string ComponentRef { - get => this.Fields[(int)GroupSymbolFields.ComponentRef].AsString(); - set => this.Set((int)GroupSymbolFields.ComponentRef, value); + get => this.Fields[(int)GroupSymbol.SymbolFields.ComponentRef].AsString(); + set => this.Set((int)GroupSymbol.SymbolFields.ComponentRef, value); } public string Name { - get => this.Fields[(int)GroupSymbolFields.Name].AsString(); - set => this.Set((int)GroupSymbolFields.Name, value); + get => this.Fields[(int)GroupSymbol.SymbolFields.Name].AsString(); + set => this.Set((int)GroupSymbol.SymbolFields.Name, value); } public string Domain { - get => this.Fields[(int)GroupSymbolFields.Domain].AsString(); - set => this.Set((int)GroupSymbolFields.Domain, value); + get => this.Fields[(int)GroupSymbol.SymbolFields.Domain].AsString(); + set => this.Set((int)GroupSymbol.SymbolFields.Domain, value); } } -} \ No newline at end of file + + public class Group6Symbol : IntermediateSymbol + { + [Flags] + public enum SymbolAttributes + { + None = 0x00000000, + FailIfExists = 0x00000001, + UpdateIfExists = 0x00000002, + DontRemoveOnUninstall = 0x00000004, + DontCreateGroup = 0x00000008, + NonVital = 0x00000010, + RemoveComment = 0x00000020, + } + + public enum SymbolFields + { + GroupRef, + Comment, + Attributes, + } + + public Group6Symbol() : base(UtilSymbolDefinitions.Group6, null, null) + { + } + + public Group6Symbol(SourceLineNumber sourceLineNumber, Identifier id = null) : base(UtilSymbolDefinitions.Group6, sourceLineNumber, id) + { + } + + public IntermediateField this[Group6Symbol.SymbolFields index] => this.Fields[(int)index]; + + public string GroupRef + { + get => this.Fields[(int)Group6Symbol.SymbolFields.GroupRef].AsString(); + set => this.Set((int)Group6Symbol.SymbolFields.GroupRef, value); + } + + public string Comment + { + get => this.Fields[(int)Group6Symbol.SymbolFields.Comment].AsString(); + set => this.Set((int)Group6Symbol.SymbolFields.Comment, value); + } + + public SymbolAttributes Attributes + { + get => (SymbolAttributes)this.Fields[(int)Group6Symbol.SymbolFields.Attributes].AsNumber(); + set => this.Set((int)Group6Symbol.SymbolFields.Attributes, (int)value); + } + } +} diff --git a/src/ext/Util/wixext/Symbols/UtilSymbolDefinitions.cs b/src/ext/Util/wixext/Symbols/UtilSymbolDefinitions.cs index 8152868f7..43d0fca01 100644 --- a/src/ext/Util/wixext/Symbols/UtilSymbolDefinitions.cs +++ b/src/ext/Util/wixext/Symbols/UtilSymbolDefinitions.cs @@ -12,6 +12,8 @@ public enum UtilSymbolDefinitionType FileShare, FileSharePermissions, Group, + Group6, + GroupGroup, Perfmon, PerfmonManifest, PerformanceCategory, @@ -59,6 +61,12 @@ public static IntermediateSymbolDefinition ByType(UtilSymbolDefinitionType type) case UtilSymbolDefinitionType.Group: return UtilSymbolDefinitions.Group; + case UtilSymbolDefinitionType.Group6: + return UtilSymbolDefinitions.Group6; + + case UtilSymbolDefinitionType.GroupGroup: + return UtilSymbolDefinitions.GroupGroup; + case UtilSymbolDefinitionType.Perfmon: return UtilSymbolDefinitions.Perfmon; diff --git a/src/ext/Util/wixext/UtilCompiler.cs b/src/ext/Util/wixext/UtilCompiler.cs index 3bcd2c0b3..aff7dd0da 100644 --- a/src/ext/Util/wixext/UtilCompiler.cs +++ b/src/ext/Util/wixext/UtilCompiler.cs @@ -139,6 +139,9 @@ public override IComponentKeyPath ParsePossibleKeyPathElement(Intermediate inter case "TouchFile": this.ParseTouchFileElement(intermediate, section, element, componentId, componentWin64); break; + case "Group": + this.ParseGroupElement(intermediate, section, element, componentId); + break; case "User": this.ParseUserElement(intermediate, section, element, componentId); break; @@ -1357,6 +1360,8 @@ private void ParseGroupElement(Intermediate intermediate, IntermediateSection se Identifier id = null; string domain = null; string name = null; + string comment = null; + Group6Symbol.SymbolAttributes attributes = Group6Symbol.SymbolAttributes.None; foreach (var attrib in element.Attributes()) { @@ -1373,6 +1378,75 @@ private void ParseGroupElement(Intermediate intermediate, IntermediateSection se case "Domain": domain = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib); break; + case "Comment": + if (null == componentId) + { + this.Messaging.Write(UtilErrors.IllegalAttributeWithoutComponent(sourceLineNumbers, element.Name.LocalName, attrib.Name.LocalName)); + } + + comment = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib); + break; + case "CreateGroup": + if (null == componentId) + { + this.Messaging.Write(UtilErrors.IllegalAttributeWithoutComponent(sourceLineNumbers, element.Name.LocalName, attrib.Name.LocalName)); + } + + if (YesNoType.No == this.ParseHelper.GetAttributeYesNoValue(sourceLineNumbers, attrib)) + { + attributes |= Group6Symbol.SymbolAttributes.DontCreateGroup; + } + break; + case "FailIfExists": + if (null == componentId) + { + this.Messaging.Write(UtilErrors.IllegalAttributeWithoutComponent(sourceLineNumbers, element.Name.LocalName, attrib.Name.LocalName)); + } + + if (YesNoType.Yes == this.ParseHelper.GetAttributeYesNoValue(sourceLineNumbers, attrib)) + { + attributes |= Group6Symbol.SymbolAttributes.FailIfExists; + } + break; + case "UpdateIfExists": + if (null == componentId) + { + this.Messaging.Write(UtilErrors.IllegalAttributeWithoutComponent(sourceLineNumbers, element.Name.LocalName, attrib.Name.LocalName)); + } + + if (YesNoType.Yes == this.ParseHelper.GetAttributeYesNoValue(sourceLineNumbers, attrib)) + { + attributes |= Group6Symbol.SymbolAttributes.UpdateIfExists; + } + break; + case "RemoveComment": + if (YesNoType.Yes == this.ParseHelper.GetAttributeYesNoValue(sourceLineNumbers, attrib)) + { + attributes |= Group6Symbol.SymbolAttributes.RemoveComment; + } + break; + case "RemoveOnUninstall": + if (null == componentId) + { + this.Messaging.Write(UtilErrors.IllegalAttributeWithoutComponent(sourceLineNumbers, element.Name.LocalName, attrib.Name.LocalName)); + } + + if (YesNoType.No == this.ParseHelper.GetAttributeYesNoValue(sourceLineNumbers, attrib)) + { + attributes |= Group6Symbol.SymbolAttributes.DontRemoveOnUninstall; + } + break; + case "Vital": + if (null == componentId) + { + this.Messaging.Write(UtilErrors.IllegalAttributeWithoutComponent(sourceLineNumbers, element.Name.LocalName, attrib.Name.LocalName)); + } + + if (YesNoType.No == this.ParseHelper.GetAttributeYesNoValue(sourceLineNumbers, attrib)) + { + attributes |= Group6Symbol.SymbolAttributes.NonVital; + } + break; default: this.ParseHelper.UnexpectedAttribute(element, attrib); break; @@ -1389,7 +1463,40 @@ private void ParseGroupElement(Intermediate intermediate, IntermediateSection se id = this.ParseHelper.CreateIdentifier("ugr", componentId, domain, name); } - this.ParseHelper.ParseForExtensionElements(this.Context.Extensions, intermediate, section, element); + if (null == name) + { + this.Messaging.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, element.Name.LocalName, "Name")); + } + + if (null != comment && (Group6Symbol.SymbolAttributes.RemoveComment & attributes) != 0) + { + this.Messaging.Write(ErrorMessages.IllegalAttributeWithOtherAttribute(sourceLineNumbers, element.Name.LocalName, "Comment", "RemoveComment")); + } + + if (null != componentId) + { + this.ParseHelper.CreateCustomActionReference(sourceLineNumbers, section, "Wix4ConfigureGroups", this.Context.Platform, CustomActionPlatforms.X86 | CustomActionPlatforms.X64 | CustomActionPlatforms.ARM64); + } + + foreach (var child in element.Elements()) + { + if (this.Namespace == child.Name.Namespace) + { + switch (child.Name.LocalName) + { + case "GroupRef": + this.ParseGroupRefElement(intermediate, section, child, id.Id, groupType:true); + break; + default: + //this.ParseHelper.UnexpectedElement(element, child); + break; + } + } + else + { + this.ParseHelper.ParseExtensionElement(this.Context.Extensions, intermediate, section, element, child); + } + } if (!this.Messaging.EncounteredError) { @@ -1399,6 +1506,12 @@ private void ParseGroupElement(Intermediate intermediate, IntermediateSection se Name = name, Domain = domain, }); + section.AddSymbol(new Group6Symbol(sourceLineNumbers, id) + { + GroupRef = id.Id, + Comment = comment, + Attributes = attributes, + }); } } @@ -1406,8 +1519,9 @@ private void ParseGroupElement(Intermediate intermediate, IntermediateSection se /// Parses a GroupRef element /// /// Element to parse. - /// Required user id to be joined to the group. - private void ParseGroupRefElement(Intermediate intermediate, IntermediateSection section, XElement element, string userId) + /// Required child id to be joined to the group. + /// whether the child is a group (true) or a user (false) + private void ParseGroupRefElement(Intermediate intermediate, IntermediateSection section, XElement element, string childId, bool groupType=false) { var sourceLineNumbers = this.ParseHelper.GetSourceLineNumbers(element); string groupId = null; @@ -1437,11 +1551,22 @@ private void ParseGroupRefElement(Intermediate intermediate, IntermediateSection if (!this.Messaging.EncounteredError) { - section.AddSymbol(new UserGroupSymbol(sourceLineNumbers) + if (!groupType) { - UserRef = userId, - GroupRef = groupId, - }); + section.AddSymbol(new UserGroupSymbol(sourceLineNumbers) + { + UserRef = childId, + GroupRef = groupId, + }); + } + else + { + section.AddSymbol(new GroupGroupSymbol(sourceLineNumbers) + { + ChildGroupRef = childId, + ParentGroupRef = groupId, + }); + } } } @@ -3460,7 +3585,7 @@ private void ParseUserElement(Intermediate intermediate, IntermediateSection sec this.Messaging.Write(UtilErrors.IllegalElementWithoutComponent(childSourceLineNumbers, child.Name.LocalName)); } - this.ParseGroupRefElement(intermediate, section, child, id.Id); + this.ParseGroupRefElement(intermediate, section, child, id.Id, groupType:false); break; default: this.ParseHelper.UnexpectedElement(element, child); diff --git a/src/ext/Util/wixext/UtilDecompiler.cs b/src/ext/Util/wixext/UtilDecompiler.cs index 52b648896..53b75b8d8 100644 --- a/src/ext/Util/wixext/UtilDecompiler.cs +++ b/src/ext/Util/wixext/UtilDecompiler.cs @@ -176,6 +176,14 @@ public override bool TryDecompileTable(Table table) case "Wix4Group": this.DecompileGroupTable(table); break; + case "Group6": + case "Wix6Group": + this.DecompileGroup6Table(table); + break; + case "GroupGroup": + case "Wix6GroupGroup": + this.DecompileGroupGroup6Table(table); + break; case "Perfmon": case "Wix4Perfmon": this.DecompilePerfmonTable(table); @@ -427,18 +435,60 @@ private void DecompileGroupTable(Table table) { foreach (var row in table.Rows) { - if (null != row[1]) - { - this.Messaging.Write(WarningMessages.UnrepresentableColumnValue(row.SourceLineNumbers, table.Name, "Component_", (string)row[1])); - } - this.DecompilerHelper.AddElementToRoot(UtilConstants.GroupName, new XAttribute("Id", row.FieldAsString(0)), - new XAttribute("Name", row.FieldAsString(1)), + new XAttribute("Name", row.FieldAsString(2)), AttributeIfNotNull("Domain", row, 3) ); } } + /// + /// Decompile the Group6 table. + /// + /// The table to decompile. + private void DecompileGroup6Table(Table table) + { + foreach (var row in table.Rows) + { + var groupId = row.FieldAsString(0); + if (this.DecompilerHelper.TryGetIndexedElement("Group", groupId, out var group)) + { + var attributes = (Group6Symbol.SymbolAttributes)(row.FieldAsNullableInteger(2) ?? 0); + group.Add(AttributeIfNotNull("Comment", row, 1)); + group.Add(AttributeIfTrue("FailIfExists", ((attributes & Group6Symbol.SymbolAttributes.FailIfExists) != 0))); + group.Add(AttributeIfTrue("UpdateIfExists", ((attributes & Group6Symbol.SymbolAttributes.UpdateIfExists) != 0))); + group.Add(AttributeIfTrue("DontRemoveOnUninstall", ((attributes & Group6Symbol.SymbolAttributes.DontRemoveOnUninstall) != 0))); + group.Add(AttributeIfTrue("DontCreateGroup", ((attributes & Group6Symbol.SymbolAttributes.DontCreateGroup) != 0))); + group.Add(AttributeIfTrue("NonVital", ((attributes & Group6Symbol.SymbolAttributes.NonVital) != 0))); + group.Add(AttributeIfTrue("RemoveComment", ((attributes & Group6Symbol.SymbolAttributes.RemoveComment) != 0))); + } + else + { + this.Messaging.Write(WarningMessages.ExpectedForeignRow(row.SourceLineNumbers, table.Name, row.GetPrimaryKey(), "Group_", groupId, "Group")); + } + } + } + + + /// + /// Decompile the GroupGroup6 table. + /// + /// The table to decompile. + private void DecompileGroupGroup6Table(Table table) + { + foreach (var row in table.Rows) + { + var childId = row.FieldAsString(1); + if (this.DecompilerHelper.TryGetIndexedElement("Group", childId, out var group)) + { + group.Add(new XElement(UtilConstants.GroupRefName, new XAttribute("Id", row.FieldAsString(0)))); + } + else + { + this.Messaging.Write(WarningMessages.ExpectedForeignRow(row.SourceLineNumbers, table.Name, row.GetPrimaryKey(), "Parent_", childId, "Group")); + } + } + } /// /// Decompile the WixInternetShortcut table. diff --git a/src/ext/Util/wixext/UtilTableDefinitions.cs b/src/ext/Util/wixext/UtilTableDefinitions.cs index baa1d25b8..908b7eea7 100644 --- a/src/ext/Util/wixext/UtilTableDefinitions.cs +++ b/src/ext/Util/wixext/UtilTableDefinitions.cs @@ -105,6 +105,29 @@ public static class UtilTableDefinitions symbolIdIsPrimaryKey: true ); + public static readonly TableDefinition Wix6Group = new TableDefinition( + "Wix6Group", + UtilSymbolDefinitions.Group6, + new[] + { + new ColumnDefinition("Group_", ColumnType.String, 72, primaryKey: true, nullable: false, ColumnCategory.Identifier, keyTable: "Wix4Group", keyColumn: 1, description: "Primary key, non-localized token", modularizeType: ColumnModularizeType.Column), + new ColumnDefinition("Comment", ColumnType.String, 255, primaryKey: false, nullable: true, ColumnCategory.Formatted, description: "Group comment", modularizeType: ColumnModularizeType.Property), + new ColumnDefinition("Attributes", ColumnType.Number, 4, primaryKey: false, nullable: true, ColumnCategory.Unknown, minValue: 0, maxValue: 65535, description: "Attributes describing how to create the group"), + }, + symbolIdIsPrimaryKey: false + ); + + public static readonly TableDefinition Wix6GroupGroup = new TableDefinition( + "Wix6GroupGroup", + UtilSymbolDefinitions.GroupGroup, + new[] + { + new ColumnDefinition("Parent_", ColumnType.String, 72, primaryKey: true, nullable: false, ColumnCategory.Identifier, keyTable: "Wix4Group", keyColumn: 1, description: "Parent Group", modularizeType: ColumnModularizeType.Column), + new ColumnDefinition("Child_", ColumnType.String, 72, primaryKey: true, nullable: false, ColumnCategory.Identifier, keyTable: "Wix4Group", keyColumn: 1, description: "Child Group, a member of the Parent Group", modularizeType: ColumnModularizeType.Column), + }, + symbolIdIsPrimaryKey: false + ); + public static readonly TableDefinition Wix4InternetShortcut = new TableDefinition( "Wix4InternetShortcut", UtilSymbolDefinitions.WixInternetShortcut, @@ -302,6 +325,8 @@ public static class UtilTableDefinitions Wix4FileShare, Wix4FileSharePermissions, Wix4Group, + Wix6Group, + Wix6GroupGroup, Wix4InternetShortcut, Wix4PerformanceCategory, Wix4Perfmon, diff --git a/src/ext/Util/wixlib/UtilExtension.wxs b/src/ext/Util/wixlib/UtilExtension.wxs index bc19d89cd..c812f73d4 100644 --- a/src/ext/Util/wixlib/UtilExtension.wxs +++ b/src/ext/Util/wixlib/UtilExtension.wxs @@ -14,6 +14,14 @@ + + + + + + + + diff --git a/src/ext/Util/wixlib/UtilExtension_Platform.wxi b/src/ext/Util/wixlib/UtilExtension_Platform.wxi index 690c76c5d..df53c7d4b 100644 --- a/src/ext/Util/wixlib/UtilExtension_Platform.wxi +++ b/src/ext/Util/wixlib/UtilExtension_Platform.wxi @@ -132,6 +132,20 @@ + + + + + + + + + + + + + + diff --git a/src/ext/Util/wixlib/de-de.wxl b/src/ext/Util/wixlib/de-de.wxl index e92564cf2..3eccdbd15 100644 --- a/src/ext/Util/wixlib/de-de.wxl +++ b/src/ext/Util/wixlib/de-de.wxl @@ -7,6 +7,10 @@ + + + + diff --git a/src/ext/Util/wixlib/en-us.wxl b/src/ext/Util/wixlib/en-us.wxl index b144989e1..92357731f 100644 --- a/src/ext/Util/wixlib/en-us.wxl +++ b/src/ext/Util/wixlib/en-us.wxl @@ -7,6 +7,10 @@ + + + + diff --git a/src/ext/Util/wixlib/es-es.wxl b/src/ext/Util/wixlib/es-es.wxl index b294e419f..06ab39f05 100644 --- a/src/ext/Util/wixlib/es-es.wxl +++ b/src/ext/Util/wixlib/es-es.wxl @@ -6,6 +6,10 @@ + + + + diff --git a/src/ext/Util/wixlib/fr-fr.wxl b/src/ext/Util/wixlib/fr-fr.wxl index 4b66d7ef1..6682abbd1 100644 --- a/src/ext/Util/wixlib/fr-fr.wxl +++ b/src/ext/Util/wixlib/fr-fr.wxl @@ -6,6 +6,10 @@ + + + + diff --git a/src/ext/Util/wixlib/it-it.wxl b/src/ext/Util/wixlib/it-it.wxl index 8f8bb536c..3d0ef9ef0 100644 --- a/src/ext/Util/wixlib/it-it.wxl +++ b/src/ext/Util/wixlib/it-it.wxl @@ -7,6 +7,10 @@ + + + + diff --git a/src/ext/Util/wixlib/ja-jp.wxl b/src/ext/Util/wixlib/ja-jp.wxl index 9c8982c1a..c4575f473 100644 --- a/src/ext/Util/wixlib/ja-jp.wxl +++ b/src/ext/Util/wixlib/ja-jp.wxl @@ -7,6 +7,10 @@ + + + + diff --git a/src/ext/Util/wixlib/nl-nl.wxl b/src/ext/Util/wixlib/nl-nl.wxl index 8b68e6fdd..f382247da 100644 --- a/src/ext/Util/wixlib/nl-nl.wxl +++ b/src/ext/Util/wixlib/nl-nl.wxl @@ -7,6 +7,10 @@ + + + + diff --git a/src/ext/Util/wixlib/pt-br.wxl b/src/ext/Util/wixlib/pt-br.wxl index d575012a0..941387df2 100644 --- a/src/ext/Util/wixlib/pt-br.wxl +++ b/src/ext/Util/wixlib/pt-br.wxl @@ -6,6 +6,10 @@ + + + + diff --git a/src/ext/caerr.wxi b/src/ext/caerr.wxi index ff7ec1212..a6f18f0cd 100644 --- a/src/ext/caerr.wxi +++ b/src/ext/caerr.wxi @@ -64,6 +64,9 @@ + + + diff --git a/src/libs/wcautil/WixToolset.WcaUtil/custommsierrors.h b/src/libs/wcautil/WixToolset.WcaUtil/custommsierrors.h index f149fb314..d4bb991cc 100644 --- a/src/libs/wcautil/WixToolset.WcaUtil/custommsierrors.h +++ b/src/libs/wcautil/WixToolset.WcaUtil/custommsierrors.h @@ -87,6 +87,9 @@ #define msierrUSRFailedUserCreateExists 26404 #define msierrUSRFailedGrantLogonAsService 26405 +#define msierrGRPFailedGroupCreate 26421 +#define msierrGRPFailedGroupCreateExists 26422 + #define msierrDependencyMissingDependencies 26451 #define msierrDependencyHasDependents 26452 diff --git a/src/test/burn/WixTestTools/UserGroupVerifier.cs b/src/test/burn/WixTestTools/UserGroupVerifier.cs new file mode 100644 index 000000000..2f874057b --- /dev/null +++ b/src/test/burn/WixTestTools/UserGroupVerifier.cs @@ -0,0 +1,198 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixTestTools +{ + using System; + using System.Text; + using System.DirectoryServices; + using System.DirectoryServices.AccountManagement; + using System.Security.Principal; + using Xunit; + + /// + /// Contains methods for User Group verification + /// + public static class UserGroupVerifier + { + /// + /// Create a local group on the machine + /// + /// + /// Has to be run as an Admin + public static void CreateLocalGroup(string groupName) + { + DeleteLocalGroup(groupName); + GroupPrincipal newGroup = new GroupPrincipal(new PrincipalContext(ContextType.Machine)); + newGroup.Name = groupName; + newGroup.Description = String.Empty; + newGroup.Save(); + } + + /// + /// Deletes a local gorup from the machine + /// + /// group name to delete + /// Has to be run as an Admin + public static void DeleteLocalGroup(string groupName) + { + GroupPrincipal newGroup = GetGroup(String.Empty, groupName); + if (null != newGroup) + { + newGroup.Delete(); + } + } + + /// + /// Verifies that a group exists or not + /// + /// domain name for the group, empty for local groups + /// the group name + public static bool GroupExists(string domainName, string groupName) + { + GroupPrincipal group = GetGroup(domainName, groupName); + + return null != group; + } + + /// + /// Sets the group comment for a given group + /// + /// domain name for the group, empty for local users + /// the group name + /// comment to be set for the group + public static void SetGroupComment(string domainName, string groupName, string comment) + { + GroupPrincipal group = GetGroup(domainName, groupName); + + Assert.False(null == group, String.Format("Group '{0}' was not found under domain '{1}'.", groupName, domainName)); + + var directoryEntry = group.GetUnderlyingObject() as DirectoryEntry; + Assert.False(null == directoryEntry); + directoryEntry.Properties["Description"].Value = comment; + group.Save(); + } + + /// + /// Adds the specified group to the specified local group + /// + /// Member to add + /// Group to add too + public static void AddGroupToGroup(string memberName, string groupName) + { + DirectoryEntry localMachine; + DirectoryEntry localGroup; + + localMachine = new DirectoryEntry("WinNT://" + Environment.MachineName.ToString()); + localGroup = localMachine.Children.Find(groupName, "group"); + Assert.False(null == localGroup, String.Format("Group '{0}' was not found.", groupName)); + DirectoryEntry group = FindActiveDirectoryGroup(memberName); + localGroup.Invoke("Add", new object[] { group.Path.ToString() }); + } + + /// + /// Find the specified group in AD + /// + /// group name to lookup + /// DirectoryEntry of the group + private static DirectoryEntry FindActiveDirectoryGroup(string groupName) + { + var mLocalMachine = new DirectoryEntry("WinNT://" + Environment.MachineName.ToString()); + var mLocalEntries = mLocalMachine.Children; + + var theGroup = mLocalEntries.Find(groupName); + return theGroup; + } + + /// + /// Verifies the group comment for a given group + /// + /// domain name for the group, empty for local users + /// the group name + /// the comment to be verified + public static void VerifyGroupComment(string domainName, string groupName, string comment) + { + GroupPrincipal group = GetGroup(domainName, groupName); + + Assert.False(null == group, String.Format("Group '{0}' was not found under domain '{1}'.", groupName, domainName)); + + var directoryEntry = group.GetUnderlyingObject() as DirectoryEntry; + Assert.False(null == directoryEntry); + Assert.True(comment == (string)(directoryEntry.Properties["Description"].Value)); + } + + /// + /// Verify that a given group is member of a local group + /// + /// domain name for the group, empty for local groups + /// the member name + /// list of groups to check for membership + public static void VerifyIsMemberOf(string domainName, string memberName, params string[] groupNames) + { + IsMemberOf(domainName, memberName, true, groupNames); + } + + /// + /// Verify that a given group is NOT member of a local group + /// + /// domain name for the group, empty for local groups + /// the member name + /// list of groups to check for membership + public static void VerifyIsNotMemberOf(string domainName, string memberName, params string[] groupNames) + { + IsMemberOf(domainName, memberName, false, groupNames); + } + + /// + /// Verify that a given user is member of a local group + /// + /// domain name for the group, empty for local groups + /// the member name + /// whether the group is expected to be a member of the groups or not + /// list of groups to check for membership + private static void IsMemberOf(string domainName, string memberName, bool shouldBeMember, params string[] groupNames) + { + GroupPrincipal group = GetGroup(domainName, memberName); + Assert.False(null == group, String.Format("Group '{0}' was not found under domain '{1}'.", memberName, domainName)); + + bool missedAGroup = false; + string message = String.Empty; + foreach (string groupName in groupNames) + { + try + { + bool found = group.IsMemberOf(new PrincipalContext(ContextType.Machine), IdentityType.Name, groupName); + if (found != shouldBeMember) + { + missedAGroup = true; + message += String.Format("Group '{0}/{1}' is {2} a member of local group '{3}'. \r\n", domainName, memberName, found ? String.Empty : "NOT", groupName); + } + } + catch (System.DirectoryServices.AccountManagement.PrincipalOperationException) + { + missedAGroup = true; + message += String.Format("Local group '{0}' was not found. \r\n", groupName); + } + } + Assert.False(missedAGroup, message); + } + + /// + /// Returns the GroupPrincipal object for a given group + /// + /// Domain name to look under, if Empty the LocalMachine is assumed as the domain + /// + /// UserPrincipal Object for the group if found, or null other wise + private static GroupPrincipal GetGroup(string domainName, string groupName) + { + if (String.IsNullOrEmpty(domainName)) + { + return GroupPrincipal.FindByIdentity(new PrincipalContext(ContextType.Machine), IdentityType.Name, groupName); + } + else + { + return GroupPrincipal.FindByIdentity(new PrincipalContext(ContextType.Domain,domainName), IdentityType.Name, groupName); + } + } + } +} + diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductA/ProductA.wixproj b/src/test/msi/TestData/UtilExtensionGroupTests/ProductA/ProductA.wixproj new file mode 100644 index 000000000..3895b853d --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductA/ProductA.wixproj @@ -0,0 +1,13 @@ + + + + {A3E0B539-63F9-4B43-9E34-F33AE1C6E06D} + true + + + + + + + + \ No newline at end of file diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductA/product.wxs b/src/test/msi/TestData/UtilExtensionGroupTests/ProductA/product.wxs new file mode 100644 index 000000000..e3c143e65 --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductA/product.wxs @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductAddCommentToExistingGroup/ProductAddCommentToExistingGroup.wixproj b/src/test/msi/TestData/UtilExtensionGroupTests/ProductAddCommentToExistingGroup/ProductAddCommentToExistingGroup.wixproj new file mode 100644 index 000000000..5938e5252 --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductAddCommentToExistingGroup/ProductAddCommentToExistingGroup.wixproj @@ -0,0 +1,13 @@ + + + + {B33D3140-4AA5-469D-9DEE-AAF8F0C626DA} + true + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductAddCommentToExistingGroup/product.wxs b/src/test/msi/TestData/UtilExtensionGroupTests/ProductAddCommentToExistingGroup/product.wxs new file mode 100644 index 000000000..e01707460 --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductAddCommentToExistingGroup/product.wxs @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductCommentDelete/ProductCommentDelete.wixproj b/src/test/msi/TestData/UtilExtensionGroupTests/ProductCommentDelete/ProductCommentDelete.wixproj new file mode 100644 index 000000000..63bb23702 --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductCommentDelete/ProductCommentDelete.wixproj @@ -0,0 +1,13 @@ + + + + {9E4C301E-5F36-4A86-85BE-776E067D929D} + true + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductCommentDelete/product.wxs b/src/test/msi/TestData/UtilExtensionGroupTests/ProductCommentDelete/product.wxs new file mode 100644 index 000000000..d18248901 --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductCommentDelete/product.wxs @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductCommentFail/ProductCommentFail.wixproj b/src/test/msi/TestData/UtilExtensionGroupTests/ProductCommentFail/ProductCommentFail.wixproj new file mode 100644 index 000000000..66f308ae8 --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductCommentFail/ProductCommentFail.wixproj @@ -0,0 +1,13 @@ + + + + {85F698E0-F542-4CB4-80A1-6630D2DEB647} + true + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductCommentFail/product_fail.wxs b/src/test/msi/TestData/UtilExtensionGroupTests/ProductCommentFail/product_fail.wxs new file mode 100644 index 000000000..29b908da0 --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductCommentFail/product_fail.wxs @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductFail/ProductFail.wixproj b/src/test/msi/TestData/UtilExtensionGroupTests/ProductFail/ProductFail.wixproj new file mode 100644 index 000000000..e2fe3aa8b --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductFail/ProductFail.wixproj @@ -0,0 +1,13 @@ + + + + {91D27DAC-04C1-4160-914E-343676D36CAA} + true + + + + + + + + \ No newline at end of file diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductFail/product_fail.wxs b/src/test/msi/TestData/UtilExtensionGroupTests/ProductFail/product_fail.wxs new file mode 100644 index 000000000..fb35bc1e1 --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductFail/product_fail.wxs @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductFailIfExists/FailIfExists.wxs b/src/test/msi/TestData/UtilExtensionGroupTests/ProductFailIfExists/FailIfExists.wxs new file mode 100644 index 000000000..00f8e12de --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductFailIfExists/FailIfExists.wxs @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductFailIfExists/ProductFailIfExists.wixproj b/src/test/msi/TestData/UtilExtensionGroupTests/ProductFailIfExists/ProductFailIfExists.wixproj new file mode 100644 index 000000000..9e1a836fb --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductFailIfExists/ProductFailIfExists.wixproj @@ -0,0 +1,13 @@ + + + + {BC803822-929E-47DA-AB3A-3A62EEEA2BFB} + true + + + + + + + + \ No newline at end of file diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductNestedGroups/ProductNestedGroups.wixproj b/src/test/msi/TestData/UtilExtensionGroupTests/ProductNestedGroups/ProductNestedGroups.wixproj new file mode 100644 index 000000000..3b2e3942d --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductNestedGroups/ProductNestedGroups.wixproj @@ -0,0 +1,13 @@ + + + + {8B6C2900-44C4-42C9-879F-82F551B10C15} + true + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductNestedGroups/product.wxs b/src/test/msi/TestData/UtilExtensionGroupTests/ProductNestedGroups/product.wxs new file mode 100644 index 000000000..191d605c0 --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductNestedGroups/product.wxs @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductNewGroupWithComment/ProductNewGroupWithComment.wixproj b/src/test/msi/TestData/UtilExtensionGroupTests/ProductNewGroupWithComment/ProductNewGroupWithComment.wixproj new file mode 100644 index 000000000..aeac903a9 --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductNewGroupWithComment/ProductNewGroupWithComment.wixproj @@ -0,0 +1,13 @@ + + + + {549E1829-BBDE-42E1-968A-BEB8FC12BFC7} + true + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductNewGroupWithComment/product.wxs b/src/test/msi/TestData/UtilExtensionGroupTests/ProductNewGroupWithComment/product.wxs new file mode 100644 index 000000000..2d012b23b --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductNewGroupWithComment/product.wxs @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductNonVitalGroup/NonVitalUserGroup.wxs b/src/test/msi/TestData/UtilExtensionGroupTests/ProductNonVitalGroup/NonVitalUserGroup.wxs new file mode 100644 index 000000000..a834c76b7 --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductNonVitalGroup/NonVitalUserGroup.wxs @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductNonVitalGroup/ProductNonVitalUserGroup.wixproj b/src/test/msi/TestData/UtilExtensionGroupTests/ProductNonVitalGroup/ProductNonVitalUserGroup.wixproj new file mode 100644 index 000000000..8734224d7 --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductNonVitalGroup/ProductNonVitalUserGroup.wixproj @@ -0,0 +1,13 @@ + + + + {455C8D4F-6D59-405C-AD51-0ACC7FB91A26} + true + + + + + + + + \ No newline at end of file diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductRestrictedDomain/ProductRestrictedDomain.wixproj b/src/test/msi/TestData/UtilExtensionGroupTests/ProductRestrictedDomain/ProductRestrictedDomain.wixproj new file mode 100644 index 000000000..e4a01a3a3 --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductRestrictedDomain/ProductRestrictedDomain.wixproj @@ -0,0 +1,13 @@ + + + + {50CF526C-A862-4327-9EA3-C96AAB6FABCE} + true + + + + + + + + \ No newline at end of file diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductRestrictedDomain/RestrictedDomain.wxs b/src/test/msi/TestData/UtilExtensionGroupTests/ProductRestrictedDomain/RestrictedDomain.wxs new file mode 100644 index 000000000..edb3387c7 --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductRestrictedDomain/RestrictedDomain.wxs @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductWithCommandLineParameters/ProductWithCommandLineParameters.wixproj b/src/test/msi/TestData/UtilExtensionGroupTests/ProductWithCommandLineParameters/ProductWithCommandLineParameters.wixproj new file mode 100644 index 000000000..93a562160 --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductWithCommandLineParameters/ProductWithCommandLineParameters.wixproj @@ -0,0 +1,13 @@ + + + + {79F2CB65-1E71-42EB-AA30-51BD70C29B23} + true + + + + + + + + diff --git a/src/test/msi/TestData/UtilExtensionGroupTests/ProductWithCommandLineParameters/ProductWithCommandLineParameters.wxs b/src/test/msi/TestData/UtilExtensionGroupTests/ProductWithCommandLineParameters/ProductWithCommandLineParameters.wxs new file mode 100644 index 000000000..059ecee8d --- /dev/null +++ b/src/test/msi/TestData/UtilExtensionGroupTests/ProductWithCommandLineParameters/ProductWithCommandLineParameters.wxs @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/src/test/msi/WixToolsetTest.MsiE2E/UtilExtensionGroupTests.cs b/src/test/msi/WixToolsetTest.MsiE2E/UtilExtensionGroupTests.cs new file mode 100644 index 000000000..796c4ecdf --- /dev/null +++ b/src/test/msi/WixToolsetTest.MsiE2E/UtilExtensionGroupTests.cs @@ -0,0 +1,271 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolsetTest.MsiE2E +{ + using System; + using WixTestTools; + using Xunit; + using Xunit.Abstractions; + + public class UtilExtensionGroupTests : MsiE2ETests + { + public UtilExtensionGroupTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } + + // Verify that the users specified in the authoring are created as expected. + [RuntimeFact] + public void CanInstallAndUninstallGroups() + { + UserGroupVerifier.CreateLocalGroup("testName3"); + var productA = this.CreatePackageInstaller("ProductA"); + + productA.InstallProduct(MSIExec.MSIExecReturnCode.SUCCESS); + + // Validate New User Information. + Assert.True(UserGroupVerifier.GroupExists(String.Empty, "testName1"), String.Format("Group '{0}' was not created on Install", "testName1")); + Assert.True(UserGroupVerifier.GroupExists(String.Empty, "testName2"), String.Format("Group '{0}' was not created on Install", "testName2")); + Assert.True(UserGroupVerifier.GroupExists(String.Empty, "testName3"), String.Format("Group '{0}' was not created on Install", "testName3")); + + productA.UninstallProduct(MSIExec.MSIExecReturnCode.SUCCESS); + + // Verify Users marked as RemoveOnUninstall were removed. + Assert.False(UserGroupVerifier.GroupExists(String.Empty, "testName1"), String.Format("Group '{0}' was not removed on Uninstall", "testName1")); + Assert.True(UserGroupVerifier.GroupExists(String.Empty, "testName2"), String.Format("Group '{0}' was removed on Uninstall", "testName2")); + + // clean up + UserGroupVerifier.DeleteLocalGroup("testName1"); + UserGroupVerifier.DeleteLocalGroup("testName2"); + UserGroupVerifier.DeleteLocalGroup("testName3"); + } + + // Verify the rollback action reverts all Users changes. + [RuntimeFact] + public void CanRollbackGroups() + { + UserGroupVerifier.CreateLocalGroup("testName3"); + var productFail = this.CreatePackageInstaller("ProductFail"); + + // make sure the user accounts are deleted before we start + UserGroupVerifier.DeleteLocalGroup("testName1"); + UserGroupVerifier.DeleteLocalGroup("testName2"); + + productFail.InstallProduct(MSIExec.MSIExecReturnCode.ERROR_INSTALL_FAILURE); + + // Verify added Users were removed on rollback. + Assert.False(UserGroupVerifier.GroupExists(String.Empty, "testName1"), String.Format("Group '{0}' was not removed on Rollback", "testName1")); + Assert.False(UserGroupVerifier.GroupExists(String.Empty, "testName2"), String.Format("Group '{0}' was not removed on Rollback", "testName2")); + + // clean up + UserGroupVerifier.DeleteLocalGroup("testName1"); + UserGroupVerifier.DeleteLocalGroup("testName2"); + UserGroupVerifier.DeleteLocalGroup("testName3"); + } + + + // Verify that command-line parameters aer not blocked by repair switches. + // Original code signalled repair mode by using "-f ", which silently + // terminated the command-line parsing, ignoring any parameters that followed. + [RuntimeFact()] + public void CanRepairGroupsWithCommandLineParameters() + { + var arguments = new string[] + { + "TESTPARAMETER1=testName1", + }; + var productWithCommandLineParameters = this.CreatePackageInstaller("ProductWithCommandLineParameters"); + + // Make sure that the user doesn't exist when we start the test. + UserGroupVerifier.DeleteLocalGroup("testName1"); + + // Install + productWithCommandLineParameters.InstallProduct(MSIExec.MSIExecReturnCode.SUCCESS, arguments); + + // Repair + productWithCommandLineParameters.RepairProduct(MSIExec.MSIExecReturnCode.SUCCESS, arguments); + + // Clean up + UserGroupVerifier.DeleteLocalGroup("testName1"); + } + + + // Verify that the groups specified in the authoring are created as expected on repair. + [RuntimeFact()] + public void CanRepairGroups() + { + UserGroupVerifier.CreateLocalGroup("testName3"); + var productA = this.CreatePackageInstaller("ProductA"); + + productA.InstallProduct(MSIExec.MSIExecReturnCode.SUCCESS); + + // Validate New User Information. + UserGroupVerifier.DeleteLocalGroup("testName1"); + + productA.RepairProduct(MSIExec.MSIExecReturnCode.SUCCESS); + + // Validate New User Information. + Assert.True(UserGroupVerifier.GroupExists(String.Empty, "testName1"), String.Format("User '{0}' was not installed on Repair", "testName1")); + Assert.True(UserGroupVerifier.GroupExists(String.Empty, "testName2"), String.Format("User '{0}' was not installed after Repair", "testName2")); + + productA.UninstallProduct(MSIExec.MSIExecReturnCode.SUCCESS); + + // Verify Users marked as RemoveOnUninstall were removed. + Assert.False(UserGroupVerifier.GroupExists(String.Empty, "testName1"), String.Format("User '{0}' was not removed on Uninstall", "testName1")); + Assert.True(UserGroupVerifier.GroupExists(String.Empty, "testName2"), String.Format("User '{0}' was removed on Uninstall", "testName2")); + + // clean up + UserGroupVerifier.DeleteLocalGroup("testName1"); + UserGroupVerifier.DeleteLocalGroup("testName2"); + UserGroupVerifier.DeleteLocalGroup("testName3"); + } + + // Verify that Installation fails if FailIfExists is set. + [RuntimeFact] + public void FailsIfGroupExists() + { + var productFailIfExists = this.CreatePackageInstaller("ProductFailIfExists"); + + // Create 'existinggroup' + UserGroupVerifier.CreateLocalGroup("existinggroup"); + + try + { + productFailIfExists.InstallProduct(MSIExec.MSIExecReturnCode.ERROR_INSTALL_FAILURE); + + // Verify User still exists. + bool userExists = UserGroupVerifier.GroupExists(String.Empty, "existinggroup"); + + Assert.True(userExists, String.Format("Group '{0}' was removed on Rollback", "existinggroup")); + } + finally + { + // clean up + UserGroupVerifier.DeleteLocalGroup("existinggroup"); + } + } + + // Verify that a group cannot be created on a domain on which you don't have create user permission. + [RuntimeFact] + public void FailsIfRestrictedDomain() + { + var productRestrictedDomain = this.CreatePackageInstaller("ProductRestrictedDomain"); + + string logFile = productRestrictedDomain.InstallProduct(MSIExec.MSIExecReturnCode.ERROR_INSTALL_FAILURE, "TEMPDOMAIN=DOESNOTEXIST"); + + // Verify expected error message in the log file + Assert.True(LogVerifier.MessageInLogFile(logFile, "CreateGroup: Error 0x8007054b: failed to find Domain DOESNOTEXIST.")); + } + + // Verify that a group can be created with a group comment + [RuntimeFact] + public void CanCreateNewGroupWithComment() + { + var productNewUserWithComment = this.CreatePackageInstaller("ProductNewGroupWithComment"); + + productNewUserWithComment.InstallProduct(); + UserGroupVerifier.VerifyGroupComment(String.Empty, "testName1", "testComment1"); + + // clean up + UserGroupVerifier.DeleteLocalGroup("testName1"); + } + + // Verify that a comment can be added to an existing group + [RuntimeFact] + public void CanAddCommentToExistingGroup() + { + UserGroupVerifier.CreateLocalGroup("testName1"); + var productAddCommentToExistingUser = this.CreatePackageInstaller("ProductAddCommentToExistingGroup"); + + productAddCommentToExistingUser.InstallProduct(); + + UserGroupVerifier.VerifyGroupComment(String.Empty, "testName1", "testComment1"); + + // clean up + UserGroupVerifier.DeleteLocalGroup("testName1"); + } + + // Verify that a comment can be repaired for a new group + [RuntimeFact] + public void CanRepairCommentOfNewGroup() + { + var productNewUserWithComment = this.CreatePackageInstaller("ProductNewGroupWithComment"); + + productNewUserWithComment.InstallProduct(); + UserGroupVerifier.SetGroupComment(String.Empty, "testName1", ""); + + productNewUserWithComment.RepairProduct(); + UserGroupVerifier.VerifyGroupComment(String.Empty, "testName1", "testComment1"); + + // clean up + UserGroupVerifier.DeleteLocalGroup("testName1"); + } + + // Verify that a comment can be changed for an existing group + [RuntimeFact] + public void CanChangeCommentOfExistingGroup() + { + UserGroupVerifier.CreateLocalGroup("testName1"); + UserGroupVerifier.SetGroupComment(String.Empty, "testName1", "initialTestComment1"); + var productNewUserWithComment = this.CreatePackageInstaller("ProductNewGroupWithComment"); + + productNewUserWithComment.InstallProduct(); + UserGroupVerifier.VerifyGroupComment(String.Empty, "testName1", "testComment1"); + + // clean up + UserGroupVerifier.DeleteLocalGroup("testName1"); + } + + // Verify that a comment can be rolled back for an existing group + [RuntimeFact] + public void CanRollbackCommentOfExistingGroup() + { + UserGroupVerifier.CreateLocalGroup("testName1"); + UserGroupVerifier.SetGroupComment(String.Empty, "testName1", "initialTestComment1"); + var productCommentFail = this.CreatePackageInstaller("ProductCommentFail"); + + productCommentFail.InstallProduct(MSIExec.MSIExecReturnCode.ERROR_INSTALL_FAILURE); + + // Verify that comment change was rolled back. + UserGroupVerifier.VerifyGroupComment(String.Empty, "testName1", "initialTestComment1"); + + // clean up + UserGroupVerifier.DeleteLocalGroup("testName1"); + } + + // Verify that a comment can be deleted for an existing group + [RuntimeFact] + public void CanDeleteCommentOfExistingGroup() + { + UserGroupVerifier.CreateLocalGroup("testName1"); + UserGroupVerifier.SetGroupComment(String.Empty, "testName1", "testComment1"); + var productCommentDelete = this.CreatePackageInstaller("ProductCommentDelete"); + + productCommentDelete.InstallProduct(MSIExec.MSIExecReturnCode.SUCCESS); + + // Verify that comment was removed. + UserGroupVerifier.VerifyGroupComment(String.Empty, "testName1", ""); + + // clean up + UserGroupVerifier.DeleteLocalGroup("testName1"); + } + + // Verify that a comment can be deleted for an existing group + [RuntimeFact] + public void CanNestGroups() + { + var productNestedGroups = this.CreatePackageInstaller("ProductNestedGroups"); + + productNestedGroups.InstallProduct(MSIExec.MSIExecReturnCode.SUCCESS); + + // Verify group nested membership + UserGroupVerifier.VerifyIsMemberOf(String.Empty, "Administrators", new string[] { "testName1", "testName2" }); + UserGroupVerifier.VerifyIsMemberOf(String.Empty, "Power Users", new string[] { "testName1" }); + + UserGroupVerifier.VerifyIsNotMemberOf(String.Empty, "Administrators", new string[] { "testName3" }); + UserGroupVerifier.VerifyIsNotMemberOf(String.Empty, "Power Users", new string[] { "testName2", "testName3" }); + + // clean up + UserGroupVerifier.DeleteLocalGroup("testName1"); + UserGroupVerifier.DeleteLocalGroup("testName2"); + UserGroupVerifier.DeleteLocalGroup("testName3"); + } + } +}