1 #!powershell
2
3 # Copyright: (c) 2018, Ansible Project
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 #Requires -Module Ansible.ModuleUtils.AddType
8
9 $spec = @{
10 options = @{
11 alias = @{ type = "str" }
12 attributes = @{
13 type = "list"
14 elements = "dict"
15 options = @{
16 name = @{ type = "str"; required = $true }
17 data = @{ type = "str" }
18 data_format = @{ type = "str"; default = "text"; choices = @("base64", "text") }
19 }
20 }
21 comment = @{ type = "str" }
22 name = @{ type = "str"; required = $true }
23 persistence = @{ type = "str"; default = "local"; choices = @("enterprise", "local") }
24 secret = @{ type = "str"; no_log = $true }
25 secret_format = @{ type = "str"; default = "text"; choices = @("base64", "text") }
26 state = @{ type = "str"; default = "present"; choices = @("absent", "present") }
27 type = @{
28 type = "str"
29 required = $true
30 choices = @("domain_password", "domain_certificate", "generic_password", "generic_certificate")
31 }
32 update_secret = @{ type = "str"; default = "always"; choices = @("always", "on_create") }
33 username = @{ type = "str" }
34 }
35 required_if = @(
36 ,@("state", "present", @("username"))
37 )
38 supports_check_mode = $true
39 }
40
41 $module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
42
43 $alias = $module.Params.alias
44 $attributes = $module.Params.attributes
45 $comment = $module.Params.comment
46 $name = $module.Params.name
47 $persistence = $module.Params.persistence
48 $secret = $module.Params.secret
49 $secret_format = $module.Params.secret_format
50 $state = $module.Params.state
51 $type = $module.Params.type
52 $update_secret = $module.Params.update_secret
53 $username = $module.Params.username
54
55 $module.Diff.before = ""
56 $module.Diff.after = ""
57
58 Add-CSharpType -AnsibleModule $module -References @'
59 using Microsoft.Win32.SafeHandles;
60 using System;
61 using System.Collections.Generic;
62 using System.Linq;
63 using System.Runtime.ConstrainedExecution;
64 using System.Runtime.InteropServices;
65 using System.Text;
66
67 namespace Ansible.CredentialManager
68 {
69 internal class NativeHelpers
70 {
71 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
72 public class CREDENTIAL
73 {
74 public CredentialFlags Flags;
75 public CredentialType Type;
76 [MarshalAs(UnmanagedType.LPWStr)] public string TargetName;
77 [MarshalAs(UnmanagedType.LPWStr)] public string Comment;
78 public FILETIME LastWritten;
79 public UInt32 CredentialBlobSize;
80 public IntPtr CredentialBlob;
81 public CredentialPersist Persist;
82 public UInt32 AttributeCount;
83 public IntPtr Attributes;
84 [MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias;
85 [MarshalAs(UnmanagedType.LPWStr)] public string UserName;
86
87 public static explicit operator Credential(CREDENTIAL v)
88 {
89 byte[] secret = new byte[(int)v.CredentialBlobSize];
90 if (v.CredentialBlob != IntPtr.Zero)
91 Marshal.Copy(v.CredentialBlob, secret, 0, secret.Length);
92
93 List<CredentialAttribute> attributes = new List<CredentialAttribute>();
94 if (v.AttributeCount > 0)
95 {
96 CREDENTIAL_ATTRIBUTE[] rawAttributes = new CREDENTIAL_ATTRIBUTE[v.AttributeCount];
97 Credential.PtrToStructureArray(rawAttributes, v.Attributes);
98 attributes = rawAttributes.Select(x => (CredentialAttribute)x).ToList();
99 }
100
101 string userName = v.UserName;
102 if (v.Type == CredentialType.DomainCertificate || v.Type == CredentialType.GenericCertificate)
103 userName = Credential.UnmarshalCertificateCredential(userName);
104
105 return new Credential
106 {
107 Type = v.Type,
108 TargetName = v.TargetName,
109 Comment = v.Comment,
110 LastWritten = (DateTimeOffset)v.LastWritten,
111 Secret = secret,
112 Persist = v.Persist,
113 Attributes = attributes,
114 TargetAlias = v.TargetAlias,
115 UserName = userName,
116 Loaded = true,
117 };
118 }
119 }
120
121 [StructLayout(LayoutKind.Sequential)]
122 public struct CREDENTIAL_ATTRIBUTE
123 {
124 [MarshalAs(UnmanagedType.LPWStr)] public string Keyword;
125 public UInt32 Flags; // Set to 0 and is reserved
126 public UInt32 ValueSize;
127 public IntPtr Value;
128
129 public static explicit operator CredentialAttribute(CREDENTIAL_ATTRIBUTE v)
130 {
131 byte[] value = new byte[v.ValueSize];
132 Marshal.Copy(v.Value, value, 0, (int)v.ValueSize);
133
134 return new CredentialAttribute
135 {
136 Keyword = v.Keyword,
137 Flags = v.Flags,
138 Value = value,
139 };
140 }
141 }
142
143 [StructLayout(LayoutKind.Sequential)]
144 public struct FILETIME
145 {
146 internal UInt32 dwLowDateTime;
147 internal UInt32 dwHighDateTime;
148
149 public static implicit operator long(FILETIME v) { return ((long)v.dwHighDateTime << 32) + v.dwLowDateTime; }
150 public static explicit operator DateTimeOffset(FILETIME v) { return DateTimeOffset.FromFileTime(v); }
151 public static explicit operator FILETIME(DateTimeOffset v)
152 {
153 return new FILETIME()
154 {
155 dwLowDateTime = (UInt32)v.ToFileTime(),
156 dwHighDateTime = ((UInt32)v.ToFileTime() >> 32),
157 };
158 }
159 }
160
161 [Flags]
162 public enum CredentialCreateFlags : uint
163 {
164 PreserveCredentialBlob = 1,
165 }
166
167 [Flags]
168 public enum CredentialFlags
169 {
170 None = 0,
171 PromptNow = 2,
172 UsernameTarget = 4,
173 }
174
175 public enum CredMarshalType : uint
176 {
177 CertCredential = 1,
178 UsernameTargetCredential,
179 BinaryBlobCredential,
180 UsernameForPackedCredential,
181 BinaryBlobForSystem,
182 }
183 }
184
185 internal class NativeMethods
186 {
187 [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
188 public static extern bool CredDeleteW(
189 [MarshalAs(UnmanagedType.LPWStr)] string TargetName,
190 CredentialType Type,
191 UInt32 Flags);
192
193 [DllImport("advapi32.dll")]
194 public static extern void CredFree(
195 IntPtr Buffer);
196
197 [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
198 public static extern bool CredMarshalCredentialW(
199 NativeHelpers.CredMarshalType CredType,
200 SafeMemoryBuffer Credential,
201 out SafeCredentialBuffer MarshaledCredential);
202
203 [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
204 public static extern bool CredReadW(
205 [MarshalAs(UnmanagedType.LPWStr)] string TargetName,
206 CredentialType Type,
207 UInt32 Flags,
208 out SafeCredentialBuffer Credential);
209
210 [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
211 public static extern bool CredUnmarshalCredentialW(
212 [MarshalAs(UnmanagedType.LPWStr)] string MarshaledCredential,
213 out NativeHelpers.CredMarshalType CredType,
214 out SafeCredentialBuffer Credential);
215
216 [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
217 public static extern bool CredWriteW(
218 NativeHelpers.CREDENTIAL Credential,
219 NativeHelpers.CredentialCreateFlags Flags);
220 }
221
222 internal class SafeCredentialBuffer : SafeHandleZeroOrMinusOneIsInvalid
223 {
224 public SafeCredentialBuffer() : base(true) { }
225
226 [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
227 protected override bool ReleaseHandle()
228 {
229 NativeMethods.CredFree(handle);
230 return true;
231 }
232 }
233
234 internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
235 {
236 public SafeMemoryBuffer() : base(true) { }
237 public SafeMemoryBuffer(int cb) : base(true)
238 {
239 base.SetHandle(Marshal.AllocHGlobal(cb));
240 }
241 public SafeMemoryBuffer(IntPtr handle) : base(true)
242 {
243 base.SetHandle(handle);
244 }
245 [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
246 protected override bool ReleaseHandle()
247 {
248 Marshal.FreeHGlobal(handle);
249 return true;
250 }
251 }
252
253 public class Win32Exception : System.ComponentModel.Win32Exception
254 {
255 private string _exception_msg;
256 public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
257 public Win32Exception(int errorCode, string message) : base(errorCode)
258 {
259 _exception_msg = String.Format("{0} - {1} (Win32 Error Code {2}: 0x{3})", message, base.Message, errorCode, errorCode.ToString("X8"));
260 }
261 public override string Message { get { return _exception_msg; } }
262 public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
263 }
264
265 public enum CredentialPersist
266 {
267 Session = 1,
268 LocalMachine = 2,
269 Enterprise = 3,
270 }
271
272 public enum CredentialType
273 {
274 Generic = 1,
275 DomainPassword = 2,
276 DomainCertificate = 3,
277 DomainVisiblePassword = 4,
278 GenericCertificate = 5,
279 DomainExtended = 6,
280 Maximum = 7,
281 MaximumEx = 1007,
282 }
283
284 public class CredentialAttribute
285 {
286 public string Keyword;
287 public UInt32 Flags;
288 public byte[] Value;
289 }
290
291 public class Credential
292 {
293 public CredentialType Type;
294 public string TargetName;
295 public string Comment;
296 public DateTimeOffset LastWritten;
297 public byte[] Secret;
298 public CredentialPersist Persist;
299 public List<CredentialAttribute> Attributes = new List<CredentialAttribute>();
300 public string TargetAlias;
301 public string UserName;
302
303 // Used to track whether the credential has been loaded into the store or not
304 public bool Loaded { get; internal set; }
305
306 public void Delete()
307 {
308 if (!Loaded)
309 return;
310
311 if (!NativeMethods.CredDeleteW(TargetName, Type, 0))
312 throw new Win32Exception(String.Format("CredDeleteW({0}) failed", TargetName));
313 Loaded = false;
314 }
315
316 public void Write(bool preserveExisting)
317 {
318 string userName = UserName;
319 // Convert the certificate thumbprint to the string expected
320 if (Type == CredentialType.DomainCertificate || Type == CredentialType.GenericCertificate)
321 userName = Credential.MarshalCertificateCredential(userName);
322
323 NativeHelpers.CREDENTIAL credential = new NativeHelpers.CREDENTIAL
324 {
325 Flags = NativeHelpers.CredentialFlags.None,
326 Type = Type,
327 TargetName = TargetName,
328 Comment = Comment,
329 LastWritten = new NativeHelpers.FILETIME(),
330 CredentialBlobSize = (UInt32)(Secret == null ? 0 : Secret.Length),
331 CredentialBlob = IntPtr.Zero, // Must be allocated and freed outside of this to ensure no memory leaks
332 Persist = Persist,
333 AttributeCount = (UInt32)(Attributes.Count),
334 Attributes = IntPtr.Zero, // Attributes must be allocated and freed outside of this to ensure no memory leaks
335 TargetAlias = TargetAlias,
336 UserName = userName,
337 };
338
339 using (SafeMemoryBuffer credentialBlob = new SafeMemoryBuffer((int)credential.CredentialBlobSize))
340 {
341 if (Secret != null)
342 Marshal.Copy(Secret, 0, credentialBlob.DangerousGetHandle(), Secret.Length);
343 credential.CredentialBlob = credentialBlob.DangerousGetHandle();
344
345 // Store the CREDENTIAL_ATTRIBUTE value in a safe memory buffer and make sure we dispose in all cases
346 List<SafeMemoryBuffer> attributeBuffers = new List<SafeMemoryBuffer>();
347 try
348 {
349 int attributeLength = Attributes.Sum(a => Marshal.SizeOf(typeof(NativeHelpers.CREDENTIAL_ATTRIBUTE)));
350 byte[] attributeBytes = new byte[attributeLength];
351 int offset = 0;
352 foreach (CredentialAttribute attribute in Attributes)
353 {
354 SafeMemoryBuffer attributeBuffer = new SafeMemoryBuffer(attribute.Value.Length);
355 attributeBuffers.Add(attributeBuffer);
356 if (attribute.Value != null)
357 Marshal.Copy(attribute.Value, 0, attributeBuffer.DangerousGetHandle(), attribute.Value.Length);
358
359 NativeHelpers.CREDENTIAL_ATTRIBUTE credentialAttribute = new NativeHelpers.CREDENTIAL_ATTRIBUTE
360 {
361 Keyword = attribute.Keyword,
362 Flags = attribute.Flags,
363 ValueSize = (UInt32)(attribute.Value == null ? 0 : attribute.Value.Length),
364 Value = attributeBuffer.DangerousGetHandle(),
365 };
366 int attributeStructLength = Marshal.SizeOf(typeof(NativeHelpers.CREDENTIAL_ATTRIBUTE));
367
368 byte[] attrBytes = new byte[attributeStructLength];
369 using (SafeMemoryBuffer tempBuffer = new SafeMemoryBuffer(attributeStructLength))
370 {
371 Marshal.StructureToPtr(credentialAttribute, tempBuffer.DangerousGetHandle(), false);
372 Marshal.Copy(tempBuffer.DangerousGetHandle(), attrBytes, 0, attributeStructLength);
373 }
374 Buffer.BlockCopy(attrBytes, 0, attributeBytes, offset, attributeStructLength);
375 offset += attributeStructLength;
376 }
377
378 using (SafeMemoryBuffer attributes = new SafeMemoryBuffer(attributeBytes.Length))
379 {
380 if (attributeBytes.Length != 0)
381 {
382 Marshal.Copy(attributeBytes, 0, attributes.DangerousGetHandle(), attributeBytes.Length);
383 credential.Attributes = attributes.DangerousGetHandle();
384 }
385
386 NativeHelpers.CredentialCreateFlags createFlags = 0;
387 if (preserveExisting)
388 createFlags |= NativeHelpers.CredentialCreateFlags.PreserveCredentialBlob;
389
390 if (!NativeMethods.CredWriteW(credential, createFlags))
391 throw new Win32Exception(String.Format("CredWriteW({0}) failed", TargetName));
392 }
393 }
394 finally
395 {
396 foreach (SafeMemoryBuffer attributeBuffer in attributeBuffers)
397 attributeBuffer.Dispose();
398 }
399 }
400 Loaded = true;
401 }
402
403 public static Credential GetCredential(string target, CredentialType type)
404 {
405 SafeCredentialBuffer buffer;
406 if (!NativeMethods.CredReadW(target, type, 0, out buffer))
407 {
408 int lastErr = Marshal.GetLastWin32Error();
409
410 // Not running with Become so cannot manage the user's credentials
411 if (lastErr == 0x00000520) // ERROR_NO_SUCH_LOGON_SESSION
412 throw new InvalidOperationException("Failed to access the user's credential store, run the module with become");
413 else if (lastErr == 0x00000490) // ERROR_NOT_FOUND
414 return null;
415 throw new Win32Exception(lastErr, "CredEnumerateW() failed");
416 }
417
418 using (buffer)
419 {
420 NativeHelpers.CREDENTIAL credential = (NativeHelpers.CREDENTIAL)Marshal.PtrToStructure(
421 buffer.DangerousGetHandle(), typeof(NativeHelpers.CREDENTIAL));
422 return (Credential)credential;
423 }
424 }
425
426 public static string MarshalCertificateCredential(string thumbprint)
427 {
428 // CredWriteW requires the UserName field to be the value of CredMarshalCredentialW() when writting a
429 // certificate auth. This converts the UserName property to the format required.
430
431 // While CERT_CREDENTIAL_INFO is the correct structure, we manually marshal the data in order to
432 // support different cert hash lengths in the future.
433 // https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_cert_credential_info
434 int hexLength = thumbprint.Length;
435 byte[] credInfo = new byte[sizeof(UInt32) + (hexLength / 2)];
436
437 // First field is cbSize which is a UInt32 value denoting the size of the total structure
438 Array.Copy(BitConverter.GetBytes((UInt32)credInfo.Length), credInfo, sizeof(UInt32));
439
440 // Now copy the byte representation of the thumbprint to the rest of the struct bytes
441 for (int i = 0; i < hexLength; i += 2)
442 credInfo[sizeof(UInt32) + (i / 2)] = Convert.ToByte(thumbprint.Substring(i, 2), 16);
443
444 IntPtr pCredInfo = Marshal.AllocHGlobal(credInfo.Length);
445 Marshal.Copy(credInfo, 0, pCredInfo, credInfo.Length);
446 SafeMemoryBuffer pCredential = new SafeMemoryBuffer(pCredInfo);
447
448 NativeHelpers.CredMarshalType marshalType = NativeHelpers.CredMarshalType.CertCredential;
449 using (pCredential)
450 {
451 SafeCredentialBuffer marshaledCredential;
452 if (!NativeMethods.CredMarshalCredentialW(marshalType, pCredential, out marshaledCredential))
453 throw new Win32Exception("CredMarshalCredentialW() failed");
454 using (marshaledCredential)
455 return Marshal.PtrToStringUni(marshaledCredential.DangerousGetHandle());
456 }
457 }
458
459 public static string UnmarshalCertificateCredential(string value)
460 {
461 NativeHelpers.CredMarshalType credType;
462 SafeCredentialBuffer pCredInfo;
463 if (!NativeMethods.CredUnmarshalCredentialW(value, out credType, out pCredInfo))
464 throw new Win32Exception("CredUnmarshalCredentialW() failed");
465
466 using (pCredInfo)
467 {
468 if (credType != NativeHelpers.CredMarshalType.CertCredential)
469 throw new InvalidOperationException(String.Format("Expected unmarshalled cred type of CertCredential, received {0}", credType));
470
471 byte[] structSizeBytes = new byte[sizeof(UInt32)];
472 Marshal.Copy(pCredInfo.DangerousGetHandle(), structSizeBytes, 0, sizeof(UInt32));
473 UInt32 structSize = BitConverter.ToUInt32(structSizeBytes, 0);
474
475 byte[] certInfoBytes = new byte[structSize];
476 Marshal.Copy(pCredInfo.DangerousGetHandle(), certInfoBytes, 0, certInfoBytes.Length);
477
478 StringBuilder hex = new StringBuilder((certInfoBytes.Length - sizeof(UInt32)) * 2);
479 for (int i = 4; i < certInfoBytes.Length; i++)
480 hex.AppendFormat("{0:x2}", certInfoBytes[i]);
481
482 return hex.ToString().ToUpperInvariant();
483 }
484 }
485
486 internal static void PtrToStructureArray<T>(T[] array, IntPtr ptr)
487 {
488 IntPtr ptrOffset = ptr;
489 for (int i = 0; i < array.Length; i++, ptrOffset = IntPtr.Add(ptrOffset, Marshal.SizeOf(typeof(T))))
490 array[i] = (T)Marshal.PtrToStructure(ptrOffset, typeof(T));
491 }
492 }
493 }
494 '@
495
ConvertTo-CredentialAttribute()496 Function ConvertTo-CredentialAttribute {
497 param($Attributes)
498
499 $converted_attributes = [System.Collections.Generic.List`1[Ansible.CredentialManager.CredentialAttribute]]@()
500 foreach ($attribute in $Attributes) {
501 $new_attribute = New-Object -TypeName Ansible.CredentialManager.CredentialAttribute
502 $new_attribute.Keyword = $attribute.name
503
504 if ($null -ne $attribute.data) {
505 if ($attribute.data_format -eq "base64") {
506 $new_attribute.Value = [System.Convert]::FromBase64String($attribute.data)
507 } else {
508 $new_attribute.Value = [System.Text.Encoding]::UTF8.GetBytes($attribute.data)
509 }
510 }
511 $converted_attributes.Add($new_attribute) > $null
512 }
513
514 return ,$converted_attributes
515 }
516
Get-DiffInfo()517 Function Get-DiffInfo {
518 param($AnsibleCredential)
519
520 $diff = @{
521 alias = $AnsibleCredential.TargetAlias
522 attributes = [System.Collections.ArrayList]@()
523 comment = $AnsibleCredential.Comment
524 name = $AnsibleCredential.TargetName
525 persistence = $AnsibleCredential.Persist.ToString()
526 type = $AnsibleCredential.Type.ToString()
527 username = $AnsibleCredential.UserName
528 }
529
530 foreach ($attribute in $AnsibleCredential.Attributes) {
531 $attribute_info = @{
532 name = $attribute.Keyword
533 data = $null
534 }
535 if ($null -ne $attribute.Value) {
536 $attribute_info.data = [System.Convert]::ToBase64String($attribute.Value)
537 }
538 $diff.attributes.Add($attribute_info) > $null
539 }
540
541 return ,$diff
542 }
543
544 # If the username is a certificate thumbprint, verify it's a valid cert in the CurrentUser/Personal store
545 if ($null -ne $username -and $type -in @("domain_certificate", "generic_certificate")) {
546 # Ensure the thumbprint is upper case with no spaces or hyphens
547 $username = $username.ToUpperInvariant().Replace(" ", "").Replace("-", "")
548
549 $certificate = Get-Item -LiteralPath Cert:\CurrentUser\My\$username -ErrorAction SilentlyContinue
550 if ($null -eq $certificate) {
551 $module.FailJson("Failed to find certificate with the thumbprint $username in the CurrentUser\My store")
552 }
553 }
554
555 # Convert the input secret to a byte array
556 if ($null -ne $secret) {
557 if ($secret_format -eq "base64") {
558 $secret = [System.Convert]::FromBase64String($secret)
559 } else {
560 $secret = [System.Text.Encoding]::Unicode.GetBytes($secret)
561 }
562 }
563
564 $persistence = switch ($persistence) {
565 "local" { [Ansible.CredentialManager.CredentialPersist]::LocalMachine }
566 "enterprise" { [Ansible.CredentialManager.CredentialPersist]::Enterprise }
567 }
568
569 $type = switch ($type) {
570 "domain_password" { [Ansible.CredentialManager.CredentialType]::DomainPassword }
571 "domain_certificate" { [Ansible.CredentialManager.CredentialType]::DomainCertificate }
572 "generic_password" { [Ansible.CredentialManager.CredentialType]::Generic }
573 "generic_certificate" { [Ansible.CredentialManager.CredentialType]::GenericCertificate }
574 }
575
576 $existing_credential = [Ansible.CredentialManager.Credential]::GetCredential($name, $type)
577 if ($null -ne $existing_credential) {
578 $module.Diff.before = Get-DiffInfo -AnsibleCredential $existing_credential
579 }
580
581 if ($state -eq "absent") {
582 if ($null -ne $existing_credential) {
583 if (-not $module.CheckMode) {
584 $existing_credential.Delete()
585 }
586 $module.Result.changed = $true
587 }
588 } else {
589 if ($null -eq $existing_credential) {
590 $new_credential = New-Object -TypeName Ansible.CredentialManager.Credential
591 $new_credential.Type = $type
592 $new_credential.TargetName = $name
593 $new_credential.Comment = if ($comment) { $comment } else { [NullString]::Value }
594 $new_credential.Secret = $secret
595 $new_credential.Persist = $persistence
596 $new_credential.TargetAlias = if ($alias) { $alias } else { [NullString]::Value }
597 $new_credential.UserName = $username
598
599 if ($null -ne $attributes) {
600 $new_credential.Attributes = ConvertTo-CredentialAttribute -Attributes $attributes
601 }
602
603 if (-not $module.CheckMode) {
604 $new_credential.Write($false)
605 }
606 $module.Result.changed = $true
607 } else {
608 $changed = $false
609 $preserve_blob = $false
610
611 # make sure we do case comparison for the comment
612 if ($existing_credential.Comment -cne $comment) {
613 $existing_credential.Comment = $comment
614 $changed = $true
615 }
616
617 if ($existing_credential.Persist -ne $persistence) {
618 $existing_credential.Persist = $persistence
619 $changed = $true
620 }
621
622 if ($existing_credential.TargetAlias -ne $alias) {
623 $existing_credential.TargetAlias = $alias
624 $changed = $true
625 }
626
627 if ($existing_credential.UserName -ne $username) {
628 $existing_credential.UserName = $username
629 $changed = $true
630 }
631
632 if ($null -ne $attributes) {
633 $attribute_changed = $false
634
635 $new_attributes = ConvertTo-CredentialAttribute -Attributes $attributes
636 if ($new_attributes.Count -ne $existing_credential.Attributes.Count) {
637 $attribute_changed = $true
638 } else {
639 for ($i = 0; $i -lt $new_attributes.Count; $i++) {
640 $new_keyword = $new_attributes[$i].Keyword
641 $new_value = $new_attributes[$i].Value
642 if ($null -eq $new_value) {
643 $new_value = ""
644 } else {
645 $new_value = [System.Convert]::ToBase64String($new_value)
646 }
647
648 $existing_keyword = $existing_credential.Attributes[$i].Keyword
649 $existing_value = $existing_credential.Attributes[$i].Value
650 if ($null -eq $existing_value) {
651 $existing_value = ""
652 } else {
653 $existing_value = [System.Convert]::ToBase64String($existing_value)
654 }
655
656 if (($new_keyword -cne $existing_keyword) -or ($new_value -ne $existing_value)) {
657 $attribute_changed = $true
658 break
659 }
660 }
661 }
662
663 if ($attribute_changed) {
664 $existing_credential.Attributes = $new_attributes
665 $changed = $true
666 }
667 }
668
669 if ($null -eq $secret) {
670 # If we haven't explicitly set a secret, tell Windows to preserve the existing blob
671 $preserve_blob = $true
672 $existing_credential.Secret = $null
673 } elseif ($update_secret -eq "always") {
674 # We should only set the password if we can't read the existing one or it doesn't match our secret
675 if ($existing_credential.Secret.Length -eq 0) {
676 # We cannot read the secret so don't know if its the configured secret
677 $existing_credential.Secret = $secret
678 $changed = $true
679 } else {
680 # We can read the secret so compare with our input
681 $input_secret_b64 = [System.Convert]::ToBase64String($secret)
682 $actual_secret_b64 = [System.Convert]::ToBase64String($existing_credential.Secret)
683 if ($input_secret_b64 -ne $actual_secret_b64) {
684 $existing_credential.Secret = $secret
685 $changed = $true
686 }
687 }
688 }
689
690 if ($changed -and -not $module.CheckMode) {
691 $existing_credential.Write($preserve_blob)
692 }
693 $module.Result.changed = $changed
694 }
695
696 if ($module.CheckMode) {
697 # We cannot reliably get the credential in check mode, set it based on the input
698 $module.Diff.after = @{
699 alias = $alias
700 attributes = $attributes
701 comment = $comment
702 name = $name
703 persistence = $persistence.ToString()
704 type = $type.ToString()
705 username = $username
706 }
707 } else {
708 # Get a new copy of the credential and use that to set the after diff
709 $new_credential = [Ansible.CredentialManager.Credential]::GetCredential($name, $type)
710 $module.Diff.after = Get-DiffInfo -AnsibleCredential $new_credential
711 }
712 }
713
714 $module.ExitJson()
715