1 #!powershell
2
3 # Copyright: (c) 2020, Brian Scholer <@briantist>
4 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6 #AnsibleRequires -CSharpUtil Ansible.Basic
7
8 $spec = @{
9 supports_check_mode = $true
10 options = @{
11 source = @{
12 type = 'path'
13 default = '%LOCALAPPDATA%\Microsoft\Windows\PowerShell\PowerShellGet\PSRepositories.xml'
14 }
15 name = @{
16 type = 'list'
17 elements = 'str'
18 default = @(
19 '*'
20 )
21 }
22 exclude = @{
23 type = 'list'
24 elements = 'str'
25 }
26 profiles = @{
27 type = 'list'
28 elements = 'str'
29 default = @(
30 '*'
31 )
32 }
33 exclude_profiles = @{
34 type = 'list'
35 elements = 'str'
36 default = @(
37 'systemprofile'
38 'LocalService'
39 'NetworkService'
40 )
41 }
42 }
43 }
44
45 $module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
46
47 $module.Diff.before = @{}
48 $module.Diff.after = @{}
49
Select-Wildcard()50 function Select-Wildcard {
51 <#
52 .SYNOPSIS
53 Compares a value to an Include and Exclude list of wildcards,
54 returning the input object if a match is found
55
56 .DESCRIPTION
57 If $Property is specified, that property of the input object is
58 compared rather than the object itself, but the original object
59 is returned, not the property.
60 #>
61 [CmdletBinding()]
62 [OutputType([object])]
63 param(
64 [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
65 [object]
66 $InputObject ,
67
68 [Parameter()]
69 [String]
70 $Property ,
71
72 [Parameter()]
73 [String[]]
74 $Include ,
75
76 [Parameter()]
77 [String[]]
78 $Exclude
79 )
80
81 Process {
82 $o = if ($Property) {
83 $InputObject.($Property)
84 }
85 else {
86 $InputObject
87 }
88
89 foreach ($inc in $Include) {
90 $imatch = $o -like $inc
91 if ($imatch) {
92 break
93 }
94 }
95
96 if (-not $imatch) {
97 return
98 }
99
100 foreach ($exc in $Exclude) {
101 if ($o -like $exc) {
102 return
103 }
104 }
105
106 $InputObject
107 }
108 }
109
Get-ProfileDirectory()110 function Get-ProfileDirectory {
111 <#
112 .SYNOPSIS
113 Returns DirectoryInfo objects for each profile on the system, as reported by the registry
114
115 .DESCRIPTION
116 The special "Default" profile, used as a template for newly created users, is explicitly
117 added to the list of possible profiles returned. Public is explicitly excluded.
118 Paths reported by the registry that don't exist on the filesystem are silently skipped.
119 #>
120 [CmdletBinding()]
121 [OutputType([System.IO.DirectoryInfo])]
122 param(
123 [Parameter()]
124 [String[]]
125 $Include ,
126
127 [Parameter()]
128 [String[]]
129 $Exclude
130 )
131
132 $regPL = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList'
133
134 # note: this is a key named "Default", not the (Default) key
135 $default = Get-ItemProperty -LiteralPath $regPL | Select-Object -ExpandProperty Default
136
137 # "ProfileImagePath" is always the local side of the profile, even if roaming profiles are used
138 # This is what we want, because PSRepositories are stored in AppData/Local and don't roam
139 $profiles = (
140 @($default) +
141 (Get-ChildItem -LiteralPath $regPL | Get-ItemProperty | Select-Object -ExpandProperty ProfileImagePath)
142 ) -as [System.IO.DirectoryInfo[]]
143
144 $profiles |
145 Where-Object -Property Exists -EQ $true |
146 Select-Wildcard -Property Name -Include $Include -Exclude $Exclude
147 }
148
Compare-Hashtable()149 function Compare-Hashtable {
150 <#
151 .SYNOPSIS
152 Attempts to naively compare two hashtables by serializing them and string comparing the serialized versions
153 #>
154 [CmdletBinding()]
155 [OutputType([bool])]
156 param(
157 [Parameter(Mandatory)]
158 [hashtable]
159 $ReferenceObject ,
160
161 [Parameter(Mandatory)]
162 [hashtable]
163 $DifferenceObject ,
164
165 [Parameter()]
166 [int]
167 $Depth
168 )
169
170 if ($PSBoundParameters.ContainsKey('Depth')) {
171 $sRef = [System.Management.Automation.PSSerializer]::Serialize($ReferenceObject, $Depth)
172 $sDif = [System.Management.Automation.PSSerializer]::Serialize($DifferenceObject, $Depth)
173 }
174 else {
175 $sRef = [System.Management.Automation.PSSerializer]::Serialize($ReferenceObject)
176 $sDif = [System.Management.Automation.PSSerializer]::Serialize($DifferenceObject)
177 }
178
179 $sRef -ceq $sDif
180 }
181
182 # load the repositories from the source file
183 try {
184 $src = $module.Params.source
185 $src_repos = Import-Clixml -LiteralPath $src -ErrorAction Stop
186 }
187 catch [System.IO.FileNotFoundException] {
188 $module.FailJson("The source file '$src' was not found.", $_)
189 }
190 catch {
191 $module.FailJson("There was an error loading the source file '$src': $($_.Exception.Message).", $_)
192 }
193
194 $profiles = Get-ProfileDirectory -Include $module.Params.profiles -Exclude $module.Params.exclude_profiles
195
196 foreach ($user in $profiles) {
197 $username = $user.Name
198
199 $repo_dir = $user.FullName | Join-Path -ChildPath 'AppData\Local\Microsoft\Windows\PowerShell\PowerShellGet'
200 $repo_path = $repo_dir | Join-Path -ChildPath 'PSRepositories.xml'
201
202 if (Test-Path -LiteralPath $repo_path) {
203 $cur_repos = Import-Clixml -LiteralPath $repo_path
204 }
205 else {
206 $cur_repos = @{}
207 }
208
209 $new_repos = $cur_repos.Clone()
210 $updated = $false
211
212 $src_repos.Keys |
213 Select-Wildcard -Include $module.Params.name -Exclude $module.Params.exclude |
214 ForEach-Object -Process {
215 # explicit scope used inside ForEach-Object to satisfy lint (PSUseDeclaredVarsMoreThanAssignment)
216 # see https://github.com/PowerShell/PSScriptAnalyzer/issues/827
217 $Script:updated = $true
218 $Script:new_repos[$_] = $Script:src_repos[$_]
219 }
220
221 $module.Diff.before[$username] = $cur_repos
222 $module.Diff.after[$username] = $new_repos
223
224 if ($updated -and -not (Compare-Hashtable -ReferenceObject $cur_repos -DifferenceObject $new_repos)) {
225 if (-not $module.CheckMode) {
226 $null = New-Item -Path $repo_dir -ItemType Directory -Force -ErrorAction SilentlyContinue
227 $new_repos | Export-Clixml -LiteralPath $repo_path -Force
228 }
229 $module.Result.changed = $true
230 }
231 }
232
233 $module.ExitJson()
234