win_shortcut: add run as admin and fix shell folder idempotency (#48584)

pull/48659/head
Jordan Borean 6 years ago committed by GitHub
parent 2b1ca25e59
commit 6898f02431
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,5 @@
bugfixes:
- win_shortcut - Added idempotency checks when ``src`` is a special shell folder like ``shell:RecycleBinFolder``
minor_changes:
- win_shortcut - Added support for setting the ``Run as administrator`` flag on a shortcut pointing to an executable

@ -6,6 +6,7 @@
# Based on: http://powershellblogger.com/2016/01/create-shortcuts-lnk-or-url-files-with-powershell/ # Based on: http://powershellblogger.com/2016/01/create-shortcuts-lnk-or-url-files-with-powershell/
#AnsibleRequires -CSharpUtil Ansible.Basic #AnsibleRequires -CSharpUtil Ansible.Basic
#Requires -Module Ansible.ModuleUtils.AddType
$spec = @{ $spec = @{
options = @{ options = @{
@ -18,6 +19,7 @@ $spec = @{
icon = @{ type='path' } icon = @{ type='path' }
description = @{ type='str' } description = @{ type='str' }
windowstyle = @{ type='str'; choices=@( 'maximized', 'minimized', 'normal' ) } windowstyle = @{ type='str'; choices=@( 'maximized', 'minimized', 'normal' ) }
run_as_admin = @{ type='bool'; default=$false }
} }
supports_check_mode = $true supports_check_mode = $true
} }
@ -33,6 +35,7 @@ $hotkey = $module.Params.hotkey
$icon = $module.Params.icon $icon = $module.Params.icon
$description = $module.Params.description $description = $module.Params.description
$windowstyle = $module.Params.windowstyle $windowstyle = $module.Params.windowstyle
$run_as_admin = $module.Params.run_as_admin
# Expand environment variables on non-path types # Expand environment variables on non-path types
if ($null -ne $src) { if ($null -ne $src) {
@ -45,9 +48,192 @@ if ($null -ne $description) {
$description = [System.Environment]::ExpandEnvironmentVariables($description) $description = [System.Environment]::ExpandEnvironmentVariables($description)
} }
$module.Result.changed = $false
$module.Result.dest = $dest $module.Result.dest = $dest
$module.Result.state = $state $module.Result.state = $state
# TODO: look at consolidating other COM actions into the C# class for future compatibility
Add-CSharpType -AnsibleModule $module -References @'
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
namespace Ansible.Shortcut
{
[ComImport()]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214F9-0000-0000-C000-000000000046")]
internal interface IShellLinkW
{
// We only care about GetPath and GetIDList, omit the other methods for now
void GetPath(StringBuilder pszFile, int cch, IntPtr pfd, UInt32 fFlags);
void GetIDList(out IntPtr ppidl);
}
[ComImport()]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("45E2b4AE-B1C3-11D0-B92F-00A0C90312E1")]
internal interface IShellLinkDataList
{
void AddDataBlock(IntPtr pDataBlock);
void CopyDataBlock(uint dwSig, out IntPtr ppDataBlock);
void RemoveDataBlock(uint dwSig);
void GetFlags(out ShellLinkFlags dwFlags);
void SetFlags(ShellLinkFlags dwFlags);
}
internal class NativeHelpers
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct SHFILEINFO
{
public IntPtr hIcon;
public int iIcon;
public UInt32 dwAttributes;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 260)] public char[] szDisplayName;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 80)] public char[] szTypeName;
}
}
internal class NativeMethods
{
[DllImport("shell32.dll")]
public static extern void ILFree(
IntPtr pidl);
[DllImport("shell32.dll")]
public static extern IntPtr SHGetFileInfoW(
IntPtr pszPath,
UInt32 dwFileAttributes,
ref NativeHelpers.SHFILEINFO psfi,
int sbFileInfo,
UInt32 uFlags);
[DllImport("shell32.dll")]
public static extern int SHParseDisplayName(
[MarshalAs(UnmanagedType.LPWStr)] string pszName,
IntPtr pbc,
out IntPtr ppidl,
UInt32 sfagoIn,
out UInt32 psfgaoOut);
}
[System.Flags]
public enum ShellLinkFlags : uint
{
Default = 0x00000000,
HasIdList = 0x00000001,
HasLinkInfo = 0x00000002,
HasName = 0x00000004,
HasRelPath = 0x00000008,
HasWorkingDir = 0x00000010,
HasArgs = 0x00000020,
HasIconLocation = 0x00000040,
Unicode = 0x00000080,
ForceNoLinkInfo = 0x00000100,
HasExpSz = 0x00000200,
RunInSeparate = 0x00000400,
HasLogo3Id = 0x00000800,
HasDarwinId = 0x00001000,
RunAsUser = 0x00002000,
HasExpIconSz = 0x00004000,
NoPidlAlias = 0x00008000,
ForceUncName = 0x00010000,
RunWithShimLayer = 0x00020000,
ForceNoLinkTrack = 0x00040000,
EnableTargetMetadata = 0x00080000,
DisableLinkPathTracking = 0x00100000,
DisableKnownFolderRelativeTracking = 0x00200000,
NoKfAlias = 0x00400000,
AllowLinkToLink = 0x00800000,
UnAliasOnSave = 0x01000000,
PreferEnvironmentPath = 0x02000000,
KeepLocalIdListForUncTarget = 0x04000000,
PersistVolumeIdToRelative = 0x08000000,
Valid = 0x0FFFF7FF,
Reserved = 0x80000000
}
public class ShellLink
{
private static Guid CLSID_ShellLink = new Guid("00021401-0000-0000-C000-000000000046");
public static ShellLinkFlags GetFlags(string path)
{
IShellLinkW link = InitialiseObj(path);
ShellLinkFlags dwFlags;
((IShellLinkDataList)link).GetFlags(out dwFlags);
return dwFlags;
}
public static void SetFlags(string path, ShellLinkFlags flags)
{
IShellLinkW link = InitialiseObj(path);
((IShellLinkDataList)link).SetFlags(flags);
((IPersistFile)link).Save(null, false);
}
public static string GetTargetPath(string path)
{
IShellLinkW link = InitialiseObj(path);
StringBuilder pathSb = new StringBuilder(260);
link.GetPath(pathSb, pathSb.Capacity, IntPtr.Zero, 0);
string linkPath = pathSb.ToString();
// If the path wasn't set, try and get the path from the ItemIDList
ShellLinkFlags flags = GetFlags(path);
if (String.IsNullOrEmpty(linkPath) && ((uint)flags & (uint)ShellLinkFlags.HasIdList) == (uint)ShellLinkFlags.HasIdList)
{
IntPtr idList = IntPtr.Zero;
try
{
link.GetIDList(out idList);
linkPath = GetDisplayNameFromPidl(idList);
}
finally
{
NativeMethods.ILFree(idList);
}
}
return linkPath;
}
public static string GetDisplayNameFromPath(string path)
{
UInt32 sfgaoOut;
IntPtr pidl = IntPtr.Zero;
try
{
int res = NativeMethods.SHParseDisplayName(path, IntPtr.Zero, out pidl, 0, out sfgaoOut);
Marshal.ThrowExceptionForHR(res);
return GetDisplayNameFromPidl(pidl);
}
finally
{
NativeMethods.ILFree(pidl);
}
}
private static string GetDisplayNameFromPidl(IntPtr pidl)
{
NativeHelpers.SHFILEINFO shFileInfo = new NativeHelpers.SHFILEINFO();
UInt32 uFlags = 0x000000208; // SHGFI_DISPLAYNAME | SHGFI_PIDL
NativeMethods.SHGetFileInfoW(pidl, 0, ref shFileInfo, Marshal.SizeOf(typeof(NativeHelpers.SHFILEINFO)), uFlags);
return new string(shFileInfo.szDisplayName).TrimEnd('\0');
}
private static IShellLinkW InitialiseObj(string path)
{
IShellLinkW link = Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_ShellLink)) as IShellLinkW;
((IPersistFile)link).Load(path, 0);
return link;
}
}
}
'@
# Convert from window style name to window style id # Convert from window style name to window style id
$windowstyles = @{ $windowstyles = @{
normal = 1 normal = 1
@ -91,16 +277,31 @@ If ($state -eq "absent") {
} }
} }
If (($null -ne $src) -and ($ShortCut.TargetPath -ne $src)) {
$module.Result.changed = $true
$ShortCut.TargetPath = $src
}
$module.Result.src = $ShortCut.TargetPath
# Determine if we have a WshShortcut or WshUrlShortcut by checking the Arguments property # Determine if we have a WshShortcut or WshUrlShortcut by checking the Arguments property
# A WshUrlShortcut objects only consists of a TargetPath property # A WshUrlShortcut objects only consists of a TargetPath property
$file_shortcut = $false
If (Get-Member -InputObject $ShortCut -Name Arguments) { If (Get-Member -InputObject $ShortCut -Name Arguments) {
# File ShortCut, compare multiple properties
$file_shortcut = $true
$target_path = $ShortCut.TargetPath
If (($null -ne $src) -and ($ShortCut.TargetPath -ne $src)) {
if ((Test-Path -Path $dest) -and (-not $ShortCut.TargetPath)) {
# If the shortcut already exists but not on the COM object, we
# are dealing with a shell path like 'shell:RecycleBinFolder'.
$expanded_src = [Ansible.Shortcut.ShellLink]::GetDisplayNameFromPath($src)
$actual_src = [Ansible.Shortcut.ShellLink]::GetTargetPath($dest)
if ($expanded_src -ne $actual_src) {
$module.Result.changed = $true
$ShortCut.TargetPath = $src
}
} else {
$module.Result.changed = $true
$ShortCut.TargetPath = $src
}
$target_path = $src
}
# This is a full-featured application shortcut ! # This is a full-featured application shortcut !
If (($null -ne $arguments) -and ($ShortCut.Arguments -ne $arguments)) { If (($null -ne $arguments) -and ($ShortCut.Arguments -ne $arguments)) {
@ -139,7 +340,15 @@ If ($state -eq "absent") {
$ShortCut.WindowStyle = $windowstyles.$windowstyle $ShortCut.WindowStyle = $windowstyles.$windowstyle
} }
$module.Result.windowstyle = $windowstyleids[$ShortCut.WindowStyle] $module.Result.windowstyle = $windowstyleids[$ShortCut.WindowStyle]
} else {
# URL Shortcut, just compare the TargetPath
if (($null -ne $src) -and ($ShortCut.TargetPath -ne $src)) {
$module.Result.changed = $true
$ShortCut.TargetPath = $src
}
$target_path = $ShortCut.TargetPath
} }
$module.Result.src = $target_path
If (($module.Result.changed -eq $true) -and ($module.CheckMode -ne $true)) { If (($module.Result.changed -eq $true) -and ($module.CheckMode -ne $true)) {
Try { Try {
@ -148,6 +357,18 @@ If ($state -eq "absent") {
$module.FailJson("Failed to create shortcut '$dest'. ($($_.Exception.Message))", $_) $module.FailJson("Failed to create shortcut '$dest'. ($($_.Exception.Message))", $_)
} }
} }
if ((Test-Path -Path $dest) -and $file_shortcut) {
# Only control the run_as_admin flag if using a File Shortcut
$flags = [Ansible.Shortcut.ShellLink]::GetFlags($dest)
if ($run_as_admin -and (-not $flags.HasFlag([Ansible.Shortcut.ShellLinkFlags]::RunAsUser))) {
[Ansible.Shortcut.ShellLink]::SetFlags($dest, ($flags -bor [Ansible.Shortcut.ShellLinkFlags]::RunAsUser))
$module.Result.changed = $true
} elseif (-not $run_as_admin -and ($flags.HasFlag([Ansible.Shortcut.ShellLinkFlags]::RunAsUser))) {
[Ansible.Shortcut.ShellLink]::SetFlags($dest, ($flags -bxor [Ansible.Shortcut.ShellLinkFlags]::RunAsUser))
$module.Result.changed = $true
}
}
} }
$module.ExitJson() $module.ExitJson()

@ -62,6 +62,12 @@ options:
- When C(present), creates or updates the shortcut. - When C(present), creates or updates the shortcut.
choices: [ absent, present ] choices: [ absent, present ]
default: present default: present
run_as_admin:
description:
- When C(src) is an executable, this can control whether the shortcut will be opened as an administrator or not.
type: bool
default: no
version_added: '2.8'
author: author:
- Dag Wieers (@dagwieers) - Dag Wieers (@dagwieers)
notes: notes:

@ -25,3 +25,13 @@
win_file: win_file:
path: '%Public%\Desktop\Registry Editor.lnk' path: '%Public%\Desktop\Registry Editor.lnk'
state: absent state: absent
- name: Clean up Shell path shortcut
win_file:
path: '%Public%\bin.lnk'
state: absent
- name: Clean up Executable shortcut
win_file:
path: '%Public%\Desktop\cmd.lnk'
state: absent

@ -255,3 +255,113 @@
that: that:
- regedit_shortcut_remove_again.changed == false - regedit_shortcut_remove_again.changed == false
- regedit_shortcut_remove_again.dest == 'C:\\Users\\Public\\Desktop\\Registry Editor.lnk' - regedit_shortcut_remove_again.dest == 'C:\\Users\\Public\\Desktop\\Registry Editor.lnk'
- name: Create shortcut to shell path
win_shortcut:
dest: '%Public%\bin.lnk'
src: shell:RecycleBinFolder
state: present
register: shell_add
- name: Check there was a change
assert:
that:
- shell_add is changed
- shell_add.dest == 'C:\\Users\\Public\\bin.lnk'
- shell_add.src == 'shell:RecycleBinFolder'
- name: Create shortcut to shell path again
win_shortcut:
dest: '%Public%\bin.lnk'
src: shell:RecycleBinFolder
state: present
register: shell_add_again
- name: Check there was no change (normal mode)
assert:
that:
- not shell_add_again is changed
- shell_add_again.src == 'shell:RecycleBinFolder'
when: not in_check_mode
- name: Check there was a change (check-mode)
assert:
that:
- shell_add_again is changed
when: in_check_mode
- name: Change shortcut to another shell path
win_shortcut:
dest: '%Public%\bin.lnk'
src: shell:Start Menu
state: present
register: shell_change
- name: Check there was a change
assert:
that:
- shell_change is changed
- shell_change.src == 'shell:Start Menu'
- name: Create shortcut to an executable without run as admin
win_shortcut:
dest: '%Public%\Desktop\cmd.lnk'
src: '%SystemRoot%\System32\cmd.exe'
state: present
register: shell_exe_limited
- name: Get run as admin flag state
win_shell: |
$shortcut = "$env:Public\Desktop\cmd.lnk"
$flags = [System.BitConverter]::ToUInt32([System.IO.FIle]::ReadAllBytes($shortcut), 20)
($flags -band 0x00002000) -eq 0x00002000
register: shell_exe_limited_actual
- name: Check that run as admin flag wasn't set (normal mode)
assert:
that:
- shell_exe_limited is changed
- not shell_exe_limited_actual.stdout_lines[0]|bool
when: not in_check_mode
- name: Check that exe shortcut results in a change (check-mode)
assert:
that:
- shell_exe_limited is changed
when: in_check_mode
- name: Set shortcut to run as admin
win_shortcut:
dest: '%Public%\Desktop\cmd.lnk'
src: '%SystemRoot%\System32\cmd.exe'
run_as_admin: True
state: present
register: shell_exe_admin
- name: Get run as admin flag state
win_shell: |
$shortcut = "$env:Public\Desktop\cmd.lnk"
$flags = [System.BitConverter]::ToUInt32([System.IO.FIle]::ReadAllBytes($shortcut), 20)
($flags -band 0x00002000) -eq 0x00002000
register: shell_exe_admin_actual
- name: Check that run as admin flag was set (normal mode)
assert:
that:
- shell_exe_admin is changed
- shell_exe_admin_actual.stdout_lines[0]|bool
when: not in_check_mode
- name: Set shortcut to run as admin again
win_shortcut:
dest: '%Public%\Desktop\cmd.lnk'
src: '%SystemRoot%\System32\cmd.exe'
run_as_admin: True
state: present
register: shell_exe_admin_again
- name: Check that set run as admin wasn't changed (normal mode)
assert:
that:
- not shell_exe_admin_again is changed
when: not in_check_mode

Loading…
Cancel
Save