diff --git a/lib/ansible/modules/windows/win_credential.ps1 b/lib/ansible/modules/windows/win_credential.ps1 new file mode 100644 index 0000000000..7d44a47622 --- /dev/null +++ b/lib/ansible/modules/windows/win_credential.ps1 @@ -0,0 +1,713 @@ +#!powershell + +# Copyright: (c) 2018, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module Ansible.ModuleUtils.AddType + +$spec = @{ + options = @{ + alias = @{ type = "str" } + attributes = @{ + type = "list" + elements = "dict" + options = @{ + name = @{ type = "str"; required = $true } + data = @{ type = "str" } + data_format = @{ type = "str"; default = "text"; choices = @("base64", "text") } + } + } + comment = @{ type = "str" } + name = @{ type = "str"; required = $true } + persistence = @{ type = "str"; default = "local"; choices = @("enterprise", "local") } + secret = @{ type = "str"; no_log = $true } + secret_format = @{ type = "str"; default = "text"; choices = @("base64", "text") } + state = @{ type = "str"; default = "present"; choices = @("absent", "present") } + type = @{ + type = "str" + required = $true + choices = @("domain_password", "domain_certificate", "generic_password", "generic_certificate") + } + update_secret = @{ type = "str"; default = "always"; choices = @("always", "on_create") } + username = @{ type = "str" } + } + required_if = @( + ,@("state", "present", @("username")) + ) + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$alias = $module.Params.alias +$attributes = $module.Params.attributes +$comment = $module.Params.comment +$name = $module.Params.name +$persistence = $module.Params.persistence +$secret = $module.Params.secret +$secret_format = $module.Params.secret_format +$state = $module.Params.state +$type = $module.Params.type +$update_secret = $module.Params.update_secret +$username = $module.Params.username + +$module.Diff.before = "" +$module.Diff.after = "" + +Add-CSharpType -AnsibleModule $module -References @' +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ansible.CredentialManager +{ + internal class NativeHelpers + { + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public class CREDENTIAL + { + public CredentialFlags Flags; + public CredentialType Type; + [MarshalAs(UnmanagedType.LPWStr)] public string TargetName; + [MarshalAs(UnmanagedType.LPWStr)] public string Comment; + public FILETIME LastWritten; + public UInt32 CredentialBlobSize; + public IntPtr CredentialBlob; + public CredentialPersist Persist; + public UInt32 AttributeCount; + public IntPtr Attributes; + [MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias; + [MarshalAs(UnmanagedType.LPWStr)] public string UserName; + + public static explicit operator Credential(CREDENTIAL v) + { + byte[] secret = new byte[(int)v.CredentialBlobSize]; + if (v.CredentialBlob != IntPtr.Zero) + Marshal.Copy(v.CredentialBlob, secret, 0, secret.Length); + + List attributes = new List(); + if (v.AttributeCount > 0) + { + CREDENTIAL_ATTRIBUTE[] rawAttributes = new CREDENTIAL_ATTRIBUTE[v.AttributeCount]; + Credential.PtrToStructureArray(rawAttributes, v.Attributes); + attributes = rawAttributes.Select(x => (CredentialAttribute)x).ToList(); + } + + string userName = v.UserName; + if (v.Type == CredentialType.DomainCertificate || v.Type == CredentialType.GenericCertificate) + userName = Credential.UnmarshalCertificateCredential(userName); + + return new Credential + { + Type = v.Type, + TargetName = v.TargetName, + Comment = v.Comment, + LastWritten = (DateTimeOffset)v.LastWritten, + Secret = secret, + Persist = v.Persist, + Attributes = attributes, + TargetAlias = v.TargetAlias, + UserName = userName, + Loaded = true, + }; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct CREDENTIAL_ATTRIBUTE + { + [MarshalAs(UnmanagedType.LPWStr)] public string Keyword; + public UInt32 Flags; // Set to 0 and is reserved + public UInt32 ValueSize; + public IntPtr Value; + + public static explicit operator CredentialAttribute(CREDENTIAL_ATTRIBUTE v) + { + byte[] value = new byte[v.ValueSize]; + Marshal.Copy(v.Value, value, 0, (int)v.ValueSize); + + return new CredentialAttribute + { + Keyword = v.Keyword, + Flags = v.Flags, + Value = value, + }; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct FILETIME + { + internal UInt32 dwLowDateTime; + internal UInt32 dwHighDateTime; + + public static implicit operator long(FILETIME v) { return ((long)v.dwHighDateTime << 32) + v.dwLowDateTime; } + public static explicit operator DateTimeOffset(FILETIME v) { return DateTimeOffset.FromFileTime(v); } + public static explicit operator FILETIME(DateTimeOffset v) + { + return new FILETIME() + { + dwLowDateTime = (UInt32)v.ToFileTime(), + dwHighDateTime = ((UInt32)v.ToFileTime() >> 32), + }; + } + } + + [Flags] + public enum CredentialCreateFlags : uint + { + PreserveCredentialBlob = 1, + } + + [Flags] + public enum CredentialFlags + { + None = 0, + PromptNow = 2, + UsernameTarget = 4, + } + + public enum CredMarshalType : uint + { + CertCredential = 1, + UsernameTargetCredential, + BinaryBlobCredential, + UsernameForPackedCredential, + BinaryBlobForSystem, + } + } + + internal class NativeMethods + { + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredDeleteW( + [MarshalAs(UnmanagedType.LPWStr)] string TargetName, + CredentialType Type, + UInt32 Flags); + + [DllImport("advapi32.dll")] + public static extern void CredFree( + IntPtr Buffer); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredMarshalCredentialW( + NativeHelpers.CredMarshalType CredType, + SafeMemoryBuffer Credential, + out SafeCredentialBuffer MarshaledCredential); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredReadW( + [MarshalAs(UnmanagedType.LPWStr)] string TargetName, + CredentialType Type, + UInt32 Flags, + out SafeCredentialBuffer Credential); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredUnmarshalCredentialW( + [MarshalAs(UnmanagedType.LPWStr)] string MarshaledCredential, + out NativeHelpers.CredMarshalType CredType, + out SafeCredentialBuffer Credential); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredWriteW( + NativeHelpers.CREDENTIAL Credential, + NativeHelpers.CredentialCreateFlags Flags); + } + + internal class SafeCredentialBuffer : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeCredentialBuffer() : base(true) { } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + NativeMethods.CredFree(handle); + return true; + } + } + + internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeMemoryBuffer() : base(true) { } + public SafeMemoryBuffer(int cb) : base(true) + { + base.SetHandle(Marshal.AllocHGlobal(cb)); + } + public SafeMemoryBuffer(IntPtr handle) : base(true) + { + base.SetHandle(handle); + } + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + Marshal.FreeHGlobal(handle); + return true; + } + } + + public class Win32Exception : System.ComponentModel.Win32Exception + { + private string _exception_msg; + public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { } + public Win32Exception(int errorCode, string message) : base(errorCode) + { + _exception_msg = String.Format("{0} - {1} (Win32 Error Code {2}: 0x{3})", message, base.Message, errorCode, errorCode.ToString("X8")); + } + public override string Message { get { return _exception_msg; } } + public static explicit operator Win32Exception(string message) { return new Win32Exception(message); } + } + + public enum CredentialPersist + { + Session = 1, + LocalMachine = 2, + Enterprise = 3, + } + + public enum CredentialType + { + Generic = 1, + DomainPassword = 2, + DomainCertificate = 3, + DomainVisiblePassword = 4, + GenericCertificate = 5, + DomainExtended = 6, + Maximum = 7, + MaximumEx = 1007, + } + + public class CredentialAttribute + { + public string Keyword; + public UInt32 Flags; + public byte[] Value; + } + + public class Credential + { + public CredentialType Type; + public string TargetName; + public string Comment; + public DateTimeOffset LastWritten; + public byte[] Secret; + public CredentialPersist Persist; + public List Attributes = new List(); + public string TargetAlias; + public string UserName; + + // Used to track whether the credential has been loaded into the store or not + public bool Loaded { get; internal set; } + + public void Delete() + { + if (!Loaded) + return; + + if (!NativeMethods.CredDeleteW(TargetName, Type, 0)) + throw new Win32Exception(String.Format("CredDeleteW({0}) failed", TargetName)); + Loaded = false; + } + + public void Write(bool preserveExisting) + { + string userName = UserName; + // Convert the certificate thumbprint to the string expected + if (Type == CredentialType.DomainCertificate || Type == CredentialType.GenericCertificate) + userName = Credential.MarshalCertificateCredential(userName); + + NativeHelpers.CREDENTIAL credential = new NativeHelpers.CREDENTIAL + { + Flags = NativeHelpers.CredentialFlags.None, + Type = Type, + TargetName = TargetName, + Comment = Comment, + LastWritten = new NativeHelpers.FILETIME(), + CredentialBlobSize = (UInt32)(Secret == null ? 0 : Secret.Length), + CredentialBlob = IntPtr.Zero, // Must be allocated and freed outside of this to ensure no memory leaks + Persist = Persist, + AttributeCount = (UInt32)(Attributes.Count), + Attributes = IntPtr.Zero, // Attributes must be allocated and freed outside of this to ensure no memory leaks + TargetAlias = TargetAlias, + UserName = userName, + }; + + using (SafeMemoryBuffer credentialBlob = new SafeMemoryBuffer((int)credential.CredentialBlobSize)) + { + if (Secret != null) + Marshal.Copy(Secret, 0, credentialBlob.DangerousGetHandle(), Secret.Length); + credential.CredentialBlob = credentialBlob.DangerousGetHandle(); + + // Store the CREDENTIAL_ATTRIBUTE value in a safe memory buffer and make sure we dispose in all cases + List attributeBuffers = new List(); + try + { + int attributeLength = Attributes.Sum(a => Marshal.SizeOf(typeof(NativeHelpers.CREDENTIAL_ATTRIBUTE))); + byte[] attributeBytes = new byte[attributeLength]; + int offset = 0; + foreach (CredentialAttribute attribute in Attributes) + { + SafeMemoryBuffer attributeBuffer = new SafeMemoryBuffer(attribute.Value.Length); + attributeBuffers.Add(attributeBuffer); + if (attribute.Value != null) + Marshal.Copy(attribute.Value, 0, attributeBuffer.DangerousGetHandle(), attribute.Value.Length); + + NativeHelpers.CREDENTIAL_ATTRIBUTE credentialAttribute = new NativeHelpers.CREDENTIAL_ATTRIBUTE + { + Keyword = attribute.Keyword, + Flags = attribute.Flags, + ValueSize = (UInt32)(attribute.Value == null ? 0 : attribute.Value.Length), + Value = attributeBuffer.DangerousGetHandle(), + }; + int attributeStructLength = Marshal.SizeOf(typeof(NativeHelpers.CREDENTIAL_ATTRIBUTE)); + + byte[] attrBytes = new byte[attributeStructLength]; + using (SafeMemoryBuffer tempBuffer = new SafeMemoryBuffer(attributeStructLength)) + { + Marshal.StructureToPtr(credentialAttribute, tempBuffer.DangerousGetHandle(), false); + Marshal.Copy(tempBuffer.DangerousGetHandle(), attrBytes, 0, attributeStructLength); + } + Buffer.BlockCopy(attrBytes, 0, attributeBytes, offset, attributeStructLength); + offset += attributeStructLength; + } + + using (SafeMemoryBuffer attributes = new SafeMemoryBuffer(attributeBytes.Length)) + { + if (attributeBytes.Length != 0) + Marshal.Copy(attributeBytes, 0, attributes.DangerousGetHandle(), attributeBytes.Length); + credential.Attributes = attributes.DangerousGetHandle(); + + NativeHelpers.CredentialCreateFlags createFlags = 0; + if (preserveExisting) + createFlags |= NativeHelpers.CredentialCreateFlags.PreserveCredentialBlob; + + if (!NativeMethods.CredWriteW(credential, createFlags)) + throw new Win32Exception(String.Format("CredWriteW({0}) failed", TargetName)); + } + } + finally + { + foreach (SafeMemoryBuffer attributeBuffer in attributeBuffers) + attributeBuffer.Dispose(); + } + } + Loaded = true; + } + + public static Credential GetCredential(string target, CredentialType type) + { + SafeCredentialBuffer buffer; + if (!NativeMethods.CredReadW(target, type, 0, out buffer)) + { + int lastErr = Marshal.GetLastWin32Error(); + + // Not running with Become so cannot manage the user's credentials + if (lastErr == 0x00000520) // ERROR_NO_SUCH_LOGON_SESSION + throw new InvalidOperationException("Failed to access the user's credential store, run the module with become"); + else if (lastErr == 0x00000490) // ERROR_NOT_FOUND + return null; + throw new Win32Exception(lastErr, "CredEnumerateW() failed"); + } + + using (buffer) + { + NativeHelpers.CREDENTIAL credential = (NativeHelpers.CREDENTIAL)Marshal.PtrToStructure( + buffer.DangerousGetHandle(), typeof(NativeHelpers.CREDENTIAL)); + return (Credential)credential; + } + } + + public static string MarshalCertificateCredential(string thumbprint) + { + // CredWriteW requires the UserName field to be the value of CredMarshalCredentialW() when writting a + // certificate auth. This converts the UserName property to the format required. + + // While CERT_CREDENTIAL_INFO is the correct structure, we manually marshal the data in order to + // support different cert hash lengths in the future. + // https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_cert_credential_info + int hexLength = thumbprint.Length; + byte[] credInfo = new byte[sizeof(UInt32) + (hexLength / 2)]; + + // First field is cbSize which is a UInt32 value denoting the size of the total structure + Array.Copy(BitConverter.GetBytes((UInt32)credInfo.Length), credInfo, sizeof(UInt32)); + + // Now copy the byte representation of the thumbprint to the rest of the struct bytes + for (int i = 0; i < hexLength; i += 2) + credInfo[sizeof(UInt32) + (i / 2)] = Convert.ToByte(thumbprint.Substring(i, 2), 16); + + IntPtr pCredInfo = Marshal.AllocHGlobal(credInfo.Length); + Marshal.Copy(credInfo, 0, pCredInfo, credInfo.Length); + SafeMemoryBuffer pCredential = new SafeMemoryBuffer(pCredInfo); + + NativeHelpers.CredMarshalType marshalType = NativeHelpers.CredMarshalType.CertCredential; + using (pCredential) + { + SafeCredentialBuffer marshaledCredential; + if (!NativeMethods.CredMarshalCredentialW(marshalType, pCredential, out marshaledCredential)) + throw new Win32Exception("CredMarshalCredentialW() failed"); + using (marshaledCredential) + return Marshal.PtrToStringUni(marshaledCredential.DangerousGetHandle()); + } + } + + public static string UnmarshalCertificateCredential(string value) + { + NativeHelpers.CredMarshalType credType; + SafeCredentialBuffer pCredInfo; + if (!NativeMethods.CredUnmarshalCredentialW(value, out credType, out pCredInfo)) + throw new Win32Exception("CredUnmarshalCredentialW() failed"); + + using (pCredInfo) + { + if (credType != NativeHelpers.CredMarshalType.CertCredential) + throw new InvalidOperationException(String.Format("Expected unmarshalled cred type of CertCredential, received {0}", credType)); + + byte[] structSizeBytes = new byte[sizeof(UInt32)]; + Marshal.Copy(pCredInfo.DangerousGetHandle(), structSizeBytes, 0, sizeof(UInt32)); + UInt32 structSize = BitConverter.ToUInt32(structSizeBytes, 0); + + byte[] certInfoBytes = new byte[structSize]; + Marshal.Copy(pCredInfo.DangerousGetHandle(), certInfoBytes, 0, certInfoBytes.Length); + + StringBuilder hex = new StringBuilder((certInfoBytes.Length - sizeof(UInt32)) * 2); + for (int i = 4; i < certInfoBytes.Length; i++) + hex.AppendFormat("{0:x2}", certInfoBytes[i]); + + return hex.ToString().ToUpperInvariant(); + } + } + + internal static void PtrToStructureArray(T[] array, IntPtr ptr) + { + IntPtr ptrOffset = ptr; + for (int i = 0; i < array.Length; i++, ptrOffset = IntPtr.Add(ptrOffset, Marshal.SizeOf(typeof(T)))) + array[i] = (T)Marshal.PtrToStructure(ptrOffset, typeof(T)); + } + } +} +'@ + +Function ConvertTo-CredentialAttribute { + param($Attributes) + + $converted_attributes = [System.Collections.Generic.List`1[Ansible.CredentialManager.CredentialAttribute]]@() + foreach ($attribute in $Attributes) { + $new_attribute = New-Object -TypeName Ansible.CredentialManager.CredentialAttribute + $new_attribute.Keyword = $attribute.name + + if ($null -ne $attribute.data) { + if ($attribute.data_format -eq "base64") { + $new_attribute.Value = [System.Convert]::FromBase64String($attribute.data) + } else { + $new_attribute.Value = [System.Text.Encoding]::UTF8.GetBytes($attribute.data) + } + } + $converted_attributes.Add($new_attribute) > $null + } + + return ,$converted_attributes +} + +Function Get-DiffInfo { + param($Credential) + + $diff = @{ + alias = $Credential.TargetAlias + attributes = [System.Collections.ArrayList]@() + comment = $Credential.Comment + name = $Credential.TargetName + persistence = $Credential.Persist.ToString() + type = $Credential.Type.ToString() + username = $Credential.UserName + } + + foreach ($attribute in $Credential.Attributes) { + $attribute_info = @{ + name = $attribute.Keyword + data = $null + } + if ($null -ne $attribute.Value) { + $attribute_info.data = [System.Convert]::ToBase64String($attribute.Value) + } + $diff.attributes.Add($attribute_info) > $null + } + + return ,$diff +} + +# If the username is a certificate thumbprint, verify it's a valid cert in the CurrentUser/Personal store +if ($null -ne $username -and $type -in @("domain_certificate", "generic_certificate")) { + # Ensure the thumbprint is upper case with no spaces or hyphens + $username = $username.ToUpperInvariant().Replace(" ", "").Replace("-", "") + + $certificate = Get-Item -Path Cert:\CurrentUser\My\$username -ErrorAction SilentlyContinue + if ($null -eq $certificate) { + $module.FailJson("Failed to find certificate with the thumbprint $username in the CurrentUser\My store") + } +} + +# Convert the input secret to a byte array +if ($null -ne $secret) { + if ($secret_format -eq "base64") { + $secret = [System.Convert]::FromBase64String($secret) + } else { + $secret = [System.Text.Encoding]::UTF8.GetBytes($secret) + } +} + +$persistence = switch ($persistence) { + "local" { [Ansible.CredentialManager.CredentialPersist]::LocalMachine } + "enterprise" { [Ansible.CredentialManager.CredentialPersist]::Enterprise } +} + +$type = switch ($type) { + "domain_password" { [Ansible.CredentialManager.CredentialType]::DomainPassword } + "domain_certificate" { [Ansible.CredentialManager.CredentialType]::DomainCertificate } + "generic_password" { [Ansible.CredentialManager.CredentialType]::Generic } + "generic_certificate" { [Ansible.CredentialManager.CredentialType]::GenericCertificate } +} + +$existing_credential = [Ansible.CredentialManager.Credential]::GetCredential($name, $type) +if ($null -ne $existing_credential) { + $module.Diff.before = Get-DiffInfo -Credential $existing_credential +} + +if ($state -eq "absent") { + if ($null -ne $existing_credential) { + if (-not $module.CheckMode) { + $existing_credential.Delete() + } + $module.Result.changed = $true + } +} else { + if ($null -eq $existing_credential) { + $new_credential = New-Object -TypeName Ansible.CredentialManager.Credential + $new_credential.Type = $type + $new_credential.TargetName = $name + $new_credential.Comment = $comment + $new_credential.Secret = $secret + $new_credential.Persist = $persistence + $new_credential.TargetAlias = $alias + $new_credential.UserName = $username + + if ($null -ne $attributes) { + $new_credential.Attributes = ConvertTo-CredentialAttribute -Attributes $attributes + } + + if (-not $module.CheckMode) { + $new_credential.Write($false) + } + $module.Result.changed = $true + } else { + $changed = $false + $preserve_blob = $false + + # make sure we do case comparison for the comment + if ($existing_credential.Comment -cne $comment) { + $existing_credential.Comment = $comment + $changed = $true + } + + if ($existing_credential.Persist -ne $persistence) { + $existing_credential.Persist = $persistence + $changed = $true + } + + if ($existing_credential.TargetAlias -ne $alias) { + $existing_credential.TargetAlias = $alias + $changed = $true + } + + if ($existing_credential.UserName -ne $username) { + $existing_credential.UserName = $username + $changed = $true + } + + if ($null -ne $attributes) { + $attribute_changed = $false + + $new_attributes = ConvertTo-CredentialAttribute -Attributes $attributes + if ($new_attributes.Count -ne $existing_credential.Attributes.Count) { + $attribute_changed = $true + } else { + for ($i = 0; $i -lt $new_attributes.Count; $i++) { + $new_keyword = $new_attributes[$i].Keyword + $new_value = $new_attributes[$i].Value + if ($null -eq $new_value) { + $new_value = "" + } else { + $new_value = [System.Convert]::ToBase64String($new_value) + } + + $existing_keyword = $existing_credential.Attributes[$i].Keyword + $existing_value = $existing_credential.Attributes[$i].Value + if ($null -eq $existing_value) { + $existing_value = "" + } else { + $existing_value = [System.Convert]::ToBase64String($existing_value) + } + + if (($new_keyword -cne $existing_keyword) -or ($new_value -ne $existing_value)) { + $attribute_changed = $true + break + } + } + } + + if ($attribute_changed) { + $existing_credential.Attributes = $new_attributes + $changed = $true + } + } + + if ($null -eq $secret) { + # If we haven't explicitly set a secret, tell Windows to preserve the existing blob + $preserve_blob = $true + $existing_credential.Secret = $null + } elseif ($update_secret -eq "always") { + # We should only set the password if we can't read the existing one or it doesn't match our secret + if ($existing_credential.Secret.Length -eq 0) { + # We cannot read the secret so don't know if its the configured secret + $existing_credential.Secret = $secret + $changed = $true + } else { + # We can read the secret so compare with our input + $input_secret_b64 = [System.Convert]::ToBase64String($secret) + $actual_secret_b64 = [System.Convert]::ToBase64String($existing_credential.Secret) + if ($input_secret_b64 -ne $actual_secret_b64) { + $existing_credential.Secret = $secret + $changed = $true + } + } + } + + if ($changed -and -not $module.CheckMode) { + $existing_credential.Write($preserve_blob) + } + $module.Result.changed = $changed + } + + if ($module.CheckMode) { + # We cannot reliably get the credential in check mode, set it based on the input + $module.Diff.after = @{ + alias = $alias + attributes = $attributes + comment = $comment + name = $name + persistence = $persistence.ToString() + type = $type.ToString() + username = $username + } + } else { + # Get a new copy of the credential and use that to set the after diff + $new_credential = [Ansible.CredentialManager.Credential]::GetCredential($name, $type) + $module.Diff.after = Get-DiffInfo -Credential $new_credential + } +} + +$module.ExitJson() + diff --git a/lib/ansible/modules/windows/win_credential.py b/lib/ansible/modules/windows/win_credential.py new file mode 100644 index 0000000000..4a280f9846 --- /dev/null +++ b/lib/ansible/modules/windows/win_credential.py @@ -0,0 +1,218 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: win_credential +version_added: '2.8' +short_description: Manages Windows Credentials in the Credential Manager +description: +- Used to create and remove Windows Credentials in the Credential Manager. +- This module can manage both standard username/password credentials as well as + certificate credentials. +options: + alias: + description: + - Adds an alias for the credential. + - Typically this is the NetBIOS name of a host if I(name) is set to the DNS + name. + type: str + attributes: + description: + - A list of dicts that set application specific attributes for a + credential. + - When set, existing attributes will be compared to the list as a whole, + any differences means all attributes will be replaced. + suboptions: + name: + description: + - The key for the attribute. + - This is not a unique identifier as multiple attributes can have the + same key. + required: True + data: + description: + - The value for the attribute. + type: str + data_format: + description: + - Controls the input type for I(data). + - If C(text), I(data) is a text string that is UTF-8 encoded to bytes. + - If C(base64), I(data) is a base64 string that is base64 decoded to + bytes. + type: str + choices: + - base64 + - text + default: text + comment: + description: + - A user defined comment for the credential. + type: str + name: + description: + - The target that identifies the server or servers that the credential is + to be used for. + - If the value can be a NetBIOS name, DNS server name, DNS host name suffix + with a wildcard character (C(*)), a NetBIOS of DNS domain name that + contains a wildcard character sequence, or an asterisk. + - See C(TargetName) in U(https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_credentiala) + for more details on what this value can be. + - This is used with I(type) to produce a unique credential. + required: True + type: str + persistence: + description: + - Defines the persistence of the credential. + - If C(local), the credential will persist for all logons of the same user + on the same host. + - C(enterprise) is the same as C(local) but the credential is visible to + the same domain user when running on other hosts and not just localhost. + type: str + choices: + - enterprise + - local + default: local + secret: + description: + - The secret for the credential. + - When omitted, then no secret is used for the credential if a new + credentials is created. + - When I(type) is a password type, this is the password for I(username). + - When I(type) is a certificate type, this is the pin for the certificate. + type: str + secret_format: + description: + - Controls the input type for I(secret). + - If C(text), I(secret) is a text string that is UTF-8 encoded to bytes. + - If C(base64), I(secret) is a base64 string that is base64 decoded to + bytes. + type: str + choices: + - base64 + - text + default: text + state: + description: + - When C(absent), the credential specified by I(name) and I(type) is + removed. + - When C(present), the credential specified by I(name) and I(type) is + removed. + type: str + choices: + - absent + - present + default: present + type: + description: + - The type of credential to store. + - This is used with I(name) to produce a unique credential. + - When the type is a C(domain) type, the credential is used by Microsoft + authentication packages like Negotiate. + - When the type is a C(generic) type, the credential is not used by any + particular authentication package. + - It is recommended to use a C(domain) type as only authentication + providers can access the secret. + required: True + type: str + choices: + - domain_password + - domain_certificate + - generic_password + - generic_certificate + update_secret: + description: + - When C(always), the secret will always be updated if they differ. + - When C(on_create), the secret will only be checked/updated when it is + first created. + - If the secret cannot be retrieved and this is set to C(always), the + module will always result in a change. + type: str + choices: + - always + - on_create + default: always + username: + description: + - When I(type) is a password type, then this is the username to store for + the credential. + - When I(type) is a credential type, then this is the thumbprint as a hex + string of the certificate to use. + - When C(type=domain_password), this should be in the form of a Netlogon + (DOMAIN\Username) or a UPN (username@DOMAIN). + - If using a certificate thumbprint, the certificate must exist in the + C(CurrentUser\My) certificate store for the executing user. + type: str +notes: +- This module requires to be run with C(become) so it can access the + user's credential store. +- There can only be one credential per host and type. if a second credential is + defined that uses the same host and type, then the original credential is + overwritten. +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: Create a local only credential + win_credential: + name: server.domain.com + type: domain_password + username: DOMAIN\username + secret: Password01 + state: present + +- name: Remove a credential + win_credential: + name: server.domain.com + type: domain_password + state: absent + +- name: Create a credential with full values + win_credential: + name: server.domain.com + type: domain_password + alias: server + username: username@DOMAIN.COM + secret: Password01 + comment: Credential for server.domain.com + persistence: enterprise + attributes: + - name: Source + data: Ansible + - name: Unique Identifier + data: Y3VzdG9tIGF0dHJpYnV0ZQ== + data_format: base64 + +- name: Create a certificate credential + win_credential: + name: '*.domain.com' + type: domain_certificate + username: 0074CC4F200D27DC3877C24A92BA8EA21E6C7AF4 + state: present + +- name: Create a generic credential + win_credential: + name: smbhost + type: generic_password + username: smbuser + password: smbuser + state: present + +- name: Remove a generic credential + win_credential: + name: smbhost + type: generic_password + state: absent +''' + +RETURN = r''' +# +''' diff --git a/test/integration/targets/win_credential/aliases b/test/integration/targets/win_credential/aliases new file mode 100644 index 0000000000..3cf5b97e80 --- /dev/null +++ b/test/integration/targets/win_credential/aliases @@ -0,0 +1 @@ +shippable/windows/group3 diff --git a/test/integration/targets/win_credential/defaults/main.yml b/test/integration/targets/win_credential/defaults/main.yml new file mode 100644 index 0000000000..c6dffc0a0f --- /dev/null +++ b/test/integration/targets/win_credential/defaults/main.yml @@ -0,0 +1,19 @@ +# The certificate in files/cert.pfx was generated with the following commands +# +# cat > client.cnf < attributes = new List(); + if (v.AttributeCount > 0) + { + CREDENTIAL_ATTRIBUTE[] rawAttributes = new CREDENTIAL_ATTRIBUTE[v.AttributeCount]; + Credential.PtrToStructureArray(rawAttributes, v.Attributes); + attributes = rawAttributes.Select(x => (CredentialAttribute)x).ToList(); + } + + string userName = v.UserName; + if (v.Type == CredentialType.DomainCertificate || v.Type == CredentialType.GenericCertificate) + userName = Credential.UnmarshalCertificateCredential(userName); + + return new Credential + { + Type = v.Type, + TargetName = v.TargetName, + Comment = v.Comment, + LastWritten = (DateTimeOffset)v.LastWritten, + Secret = secret, + Persist = v.Persist, + Attributes = attributes, + TargetAlias = v.TargetAlias, + UserName = userName, + Loaded = true, + }; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct CREDENTIAL_ATTRIBUTE + { + [MarshalAs(UnmanagedType.LPWStr)] public string Keyword; + public UInt32 Flags; // Set to 0 and is reserved + public UInt32 ValueSize; + public IntPtr Value; + + public static explicit operator CredentialAttribute(CREDENTIAL_ATTRIBUTE v) + { + byte[] value = new byte[v.ValueSize]; + Marshal.Copy(v.Value, value, 0, (int)v.ValueSize); + + return new CredentialAttribute + { + Keyword = v.Keyword, + Flags = v.Flags, + Value = value, + }; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct FILETIME + { + internal UInt32 dwLowDateTime; + internal UInt32 dwHighDateTime; + + public static implicit operator long(FILETIME v) { return ((long)v.dwHighDateTime << 32) + v.dwLowDateTime; } + public static explicit operator DateTimeOffset(FILETIME v) { return DateTimeOffset.FromFileTime(v); } + public static explicit operator FILETIME(DateTimeOffset v) + { + return new FILETIME() + { + dwLowDateTime = (UInt32)v.ToFileTime(), + dwHighDateTime = ((UInt32)v.ToFileTime() >> 32), + }; + } + } + + [Flags] + public enum CredentialCreateFlags : uint + { + PreserveCredentialBlob = 1, + } + + [Flags] + public enum CredentialFlags + { + None = 0, + PromptNow = 2, + UsernameTarget = 4, + } + + public enum CredMarshalType : uint + { + CertCredential = 1, + UsernameTargetCredential, + BinaryBlobCredential, + UsernameForPackedCredential, + BinaryBlobForSystem, + } + } + + internal class NativeMethods + { + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredDeleteW( + [MarshalAs(UnmanagedType.LPWStr)] string TargetName, + CredentialType Type, + UInt32 Flags); + + [DllImport("advapi32.dll")] + public static extern void CredFree( + IntPtr Buffer); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredMarshalCredentialW( + NativeHelpers.CredMarshalType CredType, + SafeMemoryBuffer Credential, + out SafeCredentialBuffer MarshaledCredential); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredReadW( + [MarshalAs(UnmanagedType.LPWStr)] string TargetName, + CredentialType Type, + UInt32 Flags, + out SafeCredentialBuffer Credential); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredUnmarshalCredentialW( + [MarshalAs(UnmanagedType.LPWStr)] string MarshaledCredential, + out NativeHelpers.CredMarshalType CredType, + out SafeCredentialBuffer Credential); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredWriteW( + NativeHelpers.CREDENTIAL Credential, + NativeHelpers.CredentialCreateFlags Flags); + } + + internal class SafeCredentialBuffer : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeCredentialBuffer() : base(true) { } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + NativeMethods.CredFree(handle); + return true; + } + } + + internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeMemoryBuffer() : base(true) { } + public SafeMemoryBuffer(int cb) : base(true) + { + base.SetHandle(Marshal.AllocHGlobal(cb)); + } + public SafeMemoryBuffer(IntPtr handle) : base(true) + { + base.SetHandle(handle); + } + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + Marshal.FreeHGlobal(handle); + return true; + } + } + + public class Win32Exception : System.ComponentModel.Win32Exception + { + private string _exception_msg; + public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { } + public Win32Exception(int errorCode, string message) : base(errorCode) + { + _exception_msg = String.Format("{0} - {1} (Win32 Error Code {2}: 0x{3})", message, base.Message, errorCode, errorCode.ToString("X8")); + } + public override string Message { get { return _exception_msg; } } + public static explicit operator Win32Exception(string message) { return new Win32Exception(message); } + } + + public enum CredentialPersist + { + Session = 1, + LocalMachine = 2, + Enterprise = 3, + } + + public enum CredentialType + { + Generic = 1, + DomainPassword = 2, + DomainCertificate = 3, + DomainVisiblePassword = 4, + GenericCertificate = 5, + DomainExtended = 6, + Maximum = 7, + MaximumEx = 1007, + } + + public class CredentialAttribute + { + public string Keyword; + public UInt32 Flags; + public byte[] Value; + } + + public class Credential + { + public CredentialType Type; + public string TargetName; + public string Comment; + public DateTimeOffset LastWritten; + public byte[] Secret; + public CredentialPersist Persist; + public List Attributes = new List(); + public string TargetAlias; + public string UserName; + + // Used to track whether the credential has been loaded into the store or not + public bool Loaded { get; internal set; } + + public void Delete() + { + if (!Loaded) + return; + + if (!NativeMethods.CredDeleteW(TargetName, Type, 0)) + throw new Win32Exception(String.Format("CredDeleteW({0}) failed", TargetName)); + Loaded = false; + } + + public void Write(bool preserveExisting) + { + string userName = UserName; + // Convert the certificate thumbprint to the string expected + if (Type == CredentialType.DomainCertificate || Type == CredentialType.GenericCertificate) + userName = Credential.MarshalCertificateCredential(userName); + + NativeHelpers.CREDENTIAL credential = new NativeHelpers.CREDENTIAL + { + Flags = NativeHelpers.CredentialFlags.None, + Type = Type, + TargetName = TargetName, + Comment = Comment, + LastWritten = new NativeHelpers.FILETIME(), + CredentialBlobSize = (UInt32)(Secret == null ? 0 : Secret.Length), + CredentialBlob = IntPtr.Zero, // Must be allocated and freed outside of this to ensure no memory leaks + Persist = Persist, + AttributeCount = (UInt32)(Attributes.Count), + Attributes = IntPtr.Zero, // Attributes must be allocated and freed outside of this to ensure no memory leaks + TargetAlias = TargetAlias, + UserName = userName, + }; + + using (SafeMemoryBuffer credentialBlob = new SafeMemoryBuffer((int)credential.CredentialBlobSize)) + { + if (Secret != null) + Marshal.Copy(Secret, 0, credentialBlob.DangerousGetHandle(), Secret.Length); + credential.CredentialBlob = credentialBlob.DangerousGetHandle(); + + // Store the CREDENTIAL_ATTRIBUTE value in a safe memory buffer and make sure we dispose in all cases + List attributeBuffers = new List(); + try + { + int attributeLength = Attributes.Sum(a => Marshal.SizeOf(typeof(NativeHelpers.CREDENTIAL_ATTRIBUTE))); + byte[] attributeBytes = new byte[attributeLength]; + int offset = 0; + foreach (CredentialAttribute attribute in Attributes) + { + SafeMemoryBuffer attributeBuffer = new SafeMemoryBuffer(attribute.Value.Length); + attributeBuffers.Add(attributeBuffer); + if (attribute.Value != null) + Marshal.Copy(attribute.Value, 0, attributeBuffer.DangerousGetHandle(), attribute.Value.Length); + + NativeHelpers.CREDENTIAL_ATTRIBUTE credentialAttribute = new NativeHelpers.CREDENTIAL_ATTRIBUTE + { + Keyword = attribute.Keyword, + Flags = attribute.Flags, + ValueSize = (UInt32)(attribute.Value == null ? 0 : attribute.Value.Length), + Value = attributeBuffer.DangerousGetHandle(), + }; + int attributeStructLength = Marshal.SizeOf(typeof(NativeHelpers.CREDENTIAL_ATTRIBUTE)); + + byte[] attrBytes = new byte[attributeStructLength]; + using (SafeMemoryBuffer tempBuffer = new SafeMemoryBuffer(attributeStructLength)) + { + Marshal.StructureToPtr(credentialAttribute, tempBuffer.DangerousGetHandle(), false); + Marshal.Copy(tempBuffer.DangerousGetHandle(), attrBytes, 0, attributeStructLength); + } + Buffer.BlockCopy(attrBytes, 0, attributeBytes, offset, attributeStructLength); + offset += attributeStructLength; + } + + using (SafeMemoryBuffer attributes = new SafeMemoryBuffer(attributeBytes.Length)) + { + if (attributeBytes.Length != 0) + Marshal.Copy(attributeBytes, 0, attributes.DangerousGetHandle(), attributeBytes.Length); + credential.Attributes = attributes.DangerousGetHandle(); + + NativeHelpers.CredentialCreateFlags createFlags = 0; + if (preserveExisting) + createFlags |= NativeHelpers.CredentialCreateFlags.PreserveCredentialBlob; + + if (!NativeMethods.CredWriteW(credential, createFlags)) + throw new Win32Exception(String.Format("CredWriteW({0}) failed", TargetName)); + } + } + finally + { + foreach (SafeMemoryBuffer attributeBuffer in attributeBuffers) + attributeBuffer.Dispose(); + } + } + Loaded = true; + } + + public static Credential GetCredential(string target, CredentialType type) + { + SafeCredentialBuffer buffer; + if (!NativeMethods.CredReadW(target, type, 0, out buffer)) + { + int lastErr = Marshal.GetLastWin32Error(); + + // Not running with CredSSP or Become so cannot manage the user's credentials + if (lastErr == 0x00000520) // ERROR_NO_SUCH_LOGON_SESSION + throw new InvalidOperationException("Failed to access the user's credential store, run the module with become or CredSSP"); + else if (lastErr == 0x00000490) // ERROR_NOT_FOUND + return null; + throw new Win32Exception(lastErr, "CredEnumerateW() failed"); + } + + using (buffer) + { + NativeHelpers.CREDENTIAL credential = (NativeHelpers.CREDENTIAL)Marshal.PtrToStructure( + buffer.DangerousGetHandle(), typeof(NativeHelpers.CREDENTIAL)); + return (Credential)credential; + } + } + + public static string MarshalCertificateCredential(string thumbprint) + { + // CredWriteW requires the UserName field to be the value of CredMarshalCredentialW() when writting a + // certificate auth. This converts the UserName property to the format required. + + // While CERT_CREDENTIAL_INFO is the correct structure, we manually marshal the data in order to + // support different cert hash lengths in the future. + // https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_cert_credential_info + int hexLength = thumbprint.Length; + byte[] credInfo = new byte[sizeof(UInt32) + (hexLength / 2)]; + + // First field is cbSize which is a UInt32 value denoting the size of the total structure + Array.Copy(BitConverter.GetBytes((UInt32)credInfo.Length), credInfo, sizeof(UInt32)); + + // Now copy the byte representation of the thumbprint to the rest of the struct bytes + for (int i = 0; i < hexLength; i += 2) + credInfo[sizeof(UInt32) + (i / 2)] = Convert.ToByte(thumbprint.Substring(i, 2), 16); + + IntPtr pCredInfo = Marshal.AllocHGlobal(credInfo.Length); + Marshal.Copy(credInfo, 0, pCredInfo, credInfo.Length); + SafeMemoryBuffer pCredential = new SafeMemoryBuffer(pCredInfo); + + NativeHelpers.CredMarshalType marshalType = NativeHelpers.CredMarshalType.CertCredential; + using (pCredential) + { + SafeCredentialBuffer marshaledCredential; + if (!NativeMethods.CredMarshalCredentialW(marshalType, pCredential, out marshaledCredential)) + throw new Win32Exception("CredMarshalCredentialW() failed"); + using (marshaledCredential) + return Marshal.PtrToStringUni(marshaledCredential.DangerousGetHandle()); + } + } + + public static string UnmarshalCertificateCredential(string value) + { + NativeHelpers.CredMarshalType credType; + SafeCredentialBuffer pCredInfo; + if (!NativeMethods.CredUnmarshalCredentialW(value, out credType, out pCredInfo)) + throw new Win32Exception("CredUnmarshalCredentialW() failed"); + + using (pCredInfo) + { + if (credType != NativeHelpers.CredMarshalType.CertCredential) + throw new InvalidOperationException(String.Format("Expected unmarshalled cred type of CertCredential, received {0}", credType)); + + byte[] structSizeBytes = new byte[sizeof(UInt32)]; + Marshal.Copy(pCredInfo.DangerousGetHandle(), structSizeBytes, 0, sizeof(UInt32)); + UInt32 structSize = BitConverter.ToUInt32(structSizeBytes, 0); + + byte[] certInfoBytes = new byte[structSize]; + Marshal.Copy(pCredInfo.DangerousGetHandle(), certInfoBytes, 0, certInfoBytes.Length); + + StringBuilder hex = new StringBuilder((certInfoBytes.Length - sizeof(UInt32)) * 2); + for (int i = 4; i < certInfoBytes.Length; i++) + hex.AppendFormat("{0:x2}", certInfoBytes[i]); + + return hex.ToString().ToUpperInvariant(); + } + } + + internal static void PtrToStructureArray(T[] array, IntPtr ptr) + { + IntPtr ptrOffset = ptr; + for (int i = 0; i < array.Length; i++, ptrOffset = IntPtr.Add(ptrOffset, Marshal.SizeOf(typeof(T)))) + array[i] = (T)Marshal.PtrToStructure(ptrOffset, typeof(T)); + } + } +} +'@ + +$type = switch ($type) { + "domain_password" { [Ansible.CredentialManager.CredentialType]::DomainPassword } + "domain_certificate" { [Ansible.CredentialManager.CredentialType]::DomainCertificate } + "generic_password" { [Ansible.CredentialManager.CredentialType]::Generic } + "generic_certificate" { [Ansible.CredentialManager.CredentialType]::GenericCertificate } +} + +$credential = [Ansible.CredentialManager.Credential]::GetCredential($name, $type) +if ($null -ne $credential) { + $module.Result.exists = $true + $module.Result.alias = $credential.TargetAlias + $module.Result.attributes = [System.Collections.ArrayList]@() + $module.Result.comment = $credential.Comment + $module.Result.name = $credential.TargetName + $module.Result.persistence = $credential.Persist.ToString() + $module.Result.type = $credential.Type.ToString() + $module.Result.username = $credential.UserName + + if ($null -ne $credential.Secret) { + $module.Result.secret = [System.Convert]::ToBase64String($credential.Secret) + } else { + $module.Result.secret = $null + } + + foreach ($attribute in $credential.Attributes) { + $attribute_info = @{ + name = $attribute.Keyword + } + if ($null -ne $attribute.Value) { + $attribute_info.data = [System.Convert]::ToBase64String($attribute.Value) + } else { + $attribute_info.data = $null + } + $module.Result.attributes.Add($attribute_info) > $null + } +} else { + $module.Result.exists = $false +} + +$module.ExitJson() + diff --git a/test/integration/targets/win_credential/meta/main.yml b/test/integration/targets/win_credential/meta/main.yml new file mode 100644 index 0000000000..bdea853d75 --- /dev/null +++ b/test/integration/targets/win_credential/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- prepare_win_tests diff --git a/test/integration/targets/win_credential/tasks/main.yml b/test/integration/targets/win_credential/tasks/main.yml new file mode 100644 index 0000000000..67f6b946f4 --- /dev/null +++ b/test/integration/targets/win_credential/tasks/main.yml @@ -0,0 +1,64 @@ +--- +- name: ensure test dir is present + win_file: + path: '{{ test_credential_dir }}' + state: directory + +- name: copy the pfx certificate + win_copy: + src: cert.pfx + dest: '{{ test_credential_dir }}\cert.pfx' + +- name: import the pfx into the personal store + win_certificate_store: + path: '{{ test_credential_dir }}\cert.pfx' + state: present + store_location: CurrentUser + store_name: My + password: '{{ key_password }}' + vars: &become_vars + ansible_become: True + ansible_become_method: runas + ansible_become_user: '{{ ansible_user }}' + ansible_become_pass: '{{ ansible_password }}' + +- name: ensure test credentials are removed before testing + win_credential: + name: '{{ test_hostname }}' + type: '{{ item }}' + state: absent + vars: *become_vars + with_items: + - domain_password + - domain_certificate + - generic_password + - generic_certificate + +- block: + - name: run tests + include_tasks: tests.yml + + always: + - name: remove the pfx from the personal store + win_certificate_store: + state: absent + thumbprint: '{{ cert_thumbprint }}' + store_location: CurrentUser + store_name: My + + - name: remove test credentials + win_credential: + name: '{{ test_hostname }}' + type: '{{ item }}' + state: absent + vars: *become_vars + with_items: + - domain_password + - domain_certificate + - generic_password + - generic_certificate + + - name: remove test dir + win_file: + path: '{{ test_credential_dir }}' + state: absent diff --git a/test/integration/targets/win_credential/tasks/tests.yml b/test/integration/targets/win_credential/tasks/tests.yml new file mode 100644 index 0000000000..98ae256ccf --- /dev/null +++ b/test/integration/targets/win_credential/tasks/tests.yml @@ -0,0 +1,588 @@ +--- +- name: fail to run the module without become + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username + secret: password + state: present + register: fail_no_become + failed_when: '"Failed to access the user''s credential store, run the module with become" not in fail_no_become.msg' + +- name: create domain user credential (check mode) + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username + secret: password + state: present + register: domain_user_check + check_mode: True + vars: &become_vars + ansible_become: True + ansible_become_method: runas + ansible_become_user: '{{ ansible_user }}' + ansible_become_pass: '{{ ansible_password }}' + +- name: get result of create domain user credential (check mode) + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_password + register: domain_user_actual_check + vars: *become_vars + +- name: asset create domain user credential (check mode) + assert: + that: + - domain_user_check is changed + - not domain_user_actual_check.exists + +- name: create domain user credential + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username + secret: password + state: present + register: domain_user + vars: *become_vars + +- name: get result of create domain user credential + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_password + register: domain_user_actual + vars: *become_vars + +- name: asset create domain user credential + assert: + that: + - domain_user is changed + - domain_user_actual.exists + - domain_user_actual.alias == None + - domain_user_actual.attributes == [] + - domain_user_actual.comment == None + - domain_user_actual.name == test_hostname + - domain_user_actual.persistence == "LocalMachine" + - domain_user_actual.secret == "" + - domain_user_actual.type == "DomainPassword" + - domain_user_actual.username == "DOMAIN\\username" + +- name: create domain user credential again always update + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username + secret: password + state: present + register: domain_user_again_always + vars: *become_vars + +- name: create domain user credential again on_create + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username + secret: password + update_secret: on_create + state: present + register: domain_user_again_on_create + vars: *become_vars + +- name: assert create domain user credential again + assert: + that: + - domain_user_again_always is changed + - not domain_user_again_on_create is changed + +- name: update credential (check mode) + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username2 + alias: ansible + attributes: + - name: attribute 1 + data: attribute 1 value + - name: attribute 2 + data: '{{ "attribute 2 value" | b64encode }}' + data_format: base64 + comment: Credential comment + persistence: enterprise + state: present + register: update_cred_check + check_mode: True + vars: *become_vars + +- name: get result of update credential (check mode) + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_password + register: update_cred_actual_check + vars: *become_vars + +- name: assert update credential (check mode) + assert: + that: + - update_cred_check is changed + - update_cred_actual_check.exists + - update_cred_actual_check.alias == None + - update_cred_actual_check.attributes == [] + - update_cred_actual_check.comment == None + - update_cred_actual_check.name == test_hostname + - update_cred_actual_check.persistence == "LocalMachine" + - update_cred_actual_check.secret == "" + - update_cred_actual_check.type == "DomainPassword" + - update_cred_actual_check.username == "DOMAIN\\username" + +- name: update credential + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username2 + alias: ansible + attributes: + - name: attribute 1 + data: attribute 1 value + - name: attribute 2 + data: '{{ "attribute 2 value" | b64encode }}' + data_format: base64 + comment: Credential comment + persistence: enterprise + state: present + register: update_cred + vars: *become_vars + +- name: get result of update credential + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_password + register: update_cred_actual + vars: *become_vars + +- name: assert update credential + assert: + that: + - update_cred is changed + - update_cred_actual.exists + - update_cred_actual.alias == "ansible" + - update_cred_actual.attributes|count == 2 + - update_cred_actual.attributes[0].name == "attribute 1" + - update_cred_actual.attributes[0].data == "attribute 1 value"|b64encode + - update_cred_actual.attributes[1].name == "attribute 2" + - update_cred_actual.attributes[1].data == "attribute 2 value"|b64encode + - update_cred_actual.comment == "Credential comment" + - update_cred_actual.name == test_hostname + - update_cred_actual.persistence == "Enterprise" + - update_cred_actual.secret == "" + - update_cred_actual.type == "DomainPassword" + - update_cred_actual.username == "DOMAIN\\username2" + +- name: update credential again + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username2 + alias: ansible + attributes: + - name: attribute 1 + data: attribute 1 value + - name: attribute 2 + data: '{{ "attribute 2 value" | b64encode }}' + data_format: base64 + comment: Credential comment + persistence: enterprise + state: present + register: update_cred_again + vars: *become_vars + +- name: assert update credential again + assert: + that: + - not update_cred_again is changed + +- name: add new attribute + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username2 + alias: ansible + attributes: + - name: attribute 1 + data: attribute 1 value + - name: attribute 2 + data: '{{ "attribute 2 value" | b64encode }}' + data_format: base64 + - name: attribute 3 + data: attribute 3 value + comment: Credential comment + persistence: enterprise + state: present + register: add_attribute + vars: *become_vars + +- name: get result of add new attribute + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_password + register: add_attribute_actual + vars: *become_vars + +- name: assert add new attribute + assert: + that: + - add_attribute is changed + - add_attribute_actual.attributes|count == 3 + - add_attribute_actual.attributes[0].name == "attribute 1" + - add_attribute_actual.attributes[0].data == "attribute 1 value"|b64encode + - add_attribute_actual.attributes[1].name == "attribute 2" + - add_attribute_actual.attributes[1].data == "attribute 2 value"|b64encode + - add_attribute_actual.attributes[2].name == "attribute 3" + - add_attribute_actual.attributes[2].data == "attribute 3 value"|b64encode + +- name: remove attribute + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username2 + alias: ansible + attributes: + - name: attribute 1 + data: attribute 1 value + - name: attribute 2 + data: '{{ "attribute 2 value" | b64encode }}' + data_format: base64 + comment: Credential comment + persistence: enterprise + state: present + register: remove_attribute + vars: *become_vars + +- name: get result of remove attribute + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_password + register: remove_attribute_actual + vars: *become_vars + +- name: assert remove attribute + assert: + that: + - remove_attribute is changed + - remove_attribute_actual.attributes|count == 2 + - remove_attribute_actual.attributes[0].name == "attribute 1" + - remove_attribute_actual.attributes[0].data == "attribute 1 value"|b64encode + - remove_attribute_actual.attributes[1].name == "attribute 2" + - remove_attribute_actual.attributes[1].data == "attribute 2 value"|b64encode + +- name: edit attribute + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username2 + alias: ansible + attributes: + - name: attribute 1 + data: attribute 1 value new + - name: attribute 2 + data: '{{ "attribute 2 value" | b64encode }}' + data_format: base64 + comment: Credential comment + persistence: enterprise + state: present + register: edit_attribute + vars: *become_vars + +- name: get result of edit attribute + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_password + register: edit_attribute_actual + vars: *become_vars + +- name: assert remove attribute + assert: + that: + - edit_attribute is changed + - edit_attribute_actual.attributes|count == 2 + - edit_attribute_actual.attributes[0].name == "attribute 1" + - edit_attribute_actual.attributes[0].data == "attribute 1 value new"|b64encode + - edit_attribute_actual.attributes[1].name == "attribute 2" + - edit_attribute_actual.attributes[1].data == "attribute 2 value"|b64encode + +- name: remove credential (check mode) + win_credential: + name: '{{ test_hostname }}' + type: domain_password + state: absent + register: remove_cred_check + check_mode: True + vars: *become_vars + +- name: get result of remove credential (check mode) + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_password + register: remove_cred_actual_check + vars: *become_vars + +- name: assert remove credential (check mode) + assert: + that: + - remove_cred_check is changed + - remove_cred_actual_check.exists + +- name: remove credential + win_credential: + name: '{{ test_hostname }}' + type: domain_password + state: absent + register: remove_cred + vars: *become_vars + +- name: get result of remove credential + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_password + register: remove_cred_actual + vars: *become_vars + +- name: assert remove credential + assert: + that: + - remove_cred is changed + - not remove_cred_actual.exists + +- name: remove credential again + win_credential: + name: '{{ test_hostname }}' + type: domain_password + state: absent + register: remove_cred_again + vars: *become_vars + +- name: assert remove credential again + assert: + that: + - not remove_cred_again is changed + +- name: create generic password (check mode) + win_credential: + name: '{{ test_hostname }}' + type: generic_password + persistence: enterprise + username: genericuser + secret: genericpass + state: present + register: generic_password_check + check_mode: True + vars: *become_vars + +- name: get result of create generic password (check mode) + test_cred_facts: + name: '{{ test_hostname }}' + type: generic_password + register: generic_password_actual_check + vars: *become_vars + +- name: assert result of create generic password (check mode) + assert: + that: + - generic_password_check is changed + - not generic_password_actual_check.exists + +- name: create generic password + win_credential: + name: '{{ test_hostname }}' + type: generic_password + persistence: enterprise + username: genericuser + secret: genericpass + state: present + register: generic_password + vars: *become_vars + +- name: get result of create generic password + test_cred_facts: + name: '{{ test_hostname }}' + type: generic_password + register: generic_password_actual + vars: *become_vars + +- name: assert create generic password + assert: + that: + - generic_password is changed + - generic_password_actual.exists + - generic_password_actual.alias == None + - generic_password_actual.attributes == [] + - generic_password_actual.comment == None + - generic_password_actual.name == test_hostname + - generic_password_actual.persistence == "Enterprise" + - generic_password_actual.secret == "genericpass"|b64encode + - generic_password_actual.type == "Generic" + - generic_password_actual.username == "genericuser" + +- name: create generic password again + win_credential: + name: '{{ test_hostname }}' + type: generic_password + persistence: enterprise + username: genericuser + secret: genericpass + state: present + register: generic_password_again + vars: *become_vars + +- name: assert create generic password again + assert: + that: + - not generic_password_again is changed + +- name: fail to create certificate cred with invalid thumbprint + win_credential: + name: '{{ test_hostname }}' + type: domain_certificate + username: 00112233445566778899AABBCCDDEEFF00112233 + state: present + register: fail_invalid_cert + failed_when: fail_invalid_cert.msg != "Failed to find certificate with the thumbprint 00112233445566778899AABBCCDDEEFF00112233 in the CurrentUser\\My store" + vars: *become_vars + +- name: create domain certificate cred (check mode) + win_credential: + name: '{{ test_hostname }}' + type: domain_certificate + username: '{{ cert_thumbprint }}' + state: present + register: domain_cert_check + check_mode: True + vars: *become_vars + +- name: get result of create domain certificate cred (check mode) + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_certificate + register: domain_cert_actual_check + vars: *become_vars + +- name: assert create domain certificate cred (check mode) + assert: + that: + - domain_cert_check is changed + - not domain_cert_actual_check.exists + +- name: create domain certificate cred + win_credential: + name: '{{ test_hostname }}' + type: domain_certificate + username: '{{ cert_thumbprint }}' + state: present + register: domain_cert + vars: *become_vars + +- name: get result of create domain certificate cred + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_certificate + register: domain_cert_actual + vars: *become_vars + +- name: assert create domain certificate cred + assert: + that: + - domain_cert is changed + - domain_cert_actual.exists + - domain_cert_actual.alias == None + - domain_cert_actual.attributes == [] + - domain_cert_actual.comment == None + - domain_cert_actual.name == test_hostname + - domain_cert_actual.persistence == "LocalMachine" + - domain_cert_actual.secret == "" + - domain_cert_actual.type == "DomainCertificate" + - domain_cert_actual.username == cert_thumbprint + +- name: create domain certificate cred again + win_credential: + name: '{{ test_hostname }}' + type: domain_certificate + username: '{{ cert_thumbprint }}' + state: present + register: domain_cert_again + vars: *become_vars + +- name: assert create domain certificate cred again + assert: + that: + - not domain_cert_again is changed + +- name: create generic certificate cred (check mode) + win_credential: + name: '{{ test_hostname }}' + type: generic_certificate + username: '{{ cert_thumbprint }}' + secret: '{{ "pin code" | b64encode }}' + secret_format: base64 + state: present + register: generic_cert_check + check_mode: True + vars: *become_vars + +- name: get result of create generic certificate cred (check mode) + test_cred_facts: + name: '{{ test_hostname }}' + type: generic_certificate + register: generic_cert_actual_check + vars: *become_vars + +- name: assert create generic certificate cred (check mode) + assert: + that: + - generic_cert_check is changed + - not generic_cert_actual_check.exists + +- name: create generic certificate cred + win_credential: + name: '{{ test_hostname }}' + type: generic_certificate + username: '{{ cert_thumbprint }}' + secret: '{{ "pin code" | b64encode }}' + secret_format: base64 + state: present + register: generic_cert + vars: *become_vars + +- name: get result of create generic certificate cred + test_cred_facts: + name: '{{ test_hostname }}' + type: generic_certificate + register: generic_cert_actual + vars: *become_vars + +- name: assert create generic certificate cred + assert: + that: + - generic_cert is changed + - generic_cert_actual.exists + - generic_cert_actual.alias == None + - generic_cert_actual.attributes == [] + - generic_cert_actual.comment == None + - generic_cert_actual.name == test_hostname + - generic_cert_actual.persistence == "LocalMachine" + - generic_cert_actual.secret == "pin code" | b64encode + - generic_cert_actual.type == "GenericCertificate" + - generic_cert_actual.username == cert_thumbprint + +- name: create generic certificate cred again + win_credential: + name: '{{ test_hostname }}' + type: generic_certificate + username: '{{ cert_thumbprint }}' + state: present + register: generic_cert_again + vars: *become_vars + +- name: assert create generic certificate cred again + assert: + that: + - not generic_cert_again is changed diff --git a/test/sanity/pslint/ignore.txt b/test/sanity/pslint/ignore.txt index 120b502f86..580ee91f26 100644 --- a/test/sanity/pslint/ignore.txt +++ b/test/sanity/pslint/ignore.txt @@ -12,6 +12,7 @@ lib/ansible/modules/windows/setup.ps1 PSAvoidUsingEmptyCatchBlock lib/ansible/modules/windows/setup.ps1 PSUseDeclaredVarsMoreThanAssignments lib/ansible/modules/windows/win_copy.ps1 PSUseApprovedVerbs lib/ansible/modules/windows/win_copy.ps1 PSUseDeclaredVarsMoreThanAssignments +lib/ansible/modules/windows/win_credential.ps1 PSUsePSCredentialType # The Credential parameter is a custom .NET type lib/ansible/modules/windows/win_dns_client.ps1 PSAvoidGlobalVars lib/ansible/modules/windows/win_dns_client.ps1 PSAvoidUsingCmdletAliases lib/ansible/modules/windows/win_dns_client.ps1 PSAvoidUsingWMICmdlet