mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
win_credential: new module to manage credentials (#48840)
* win_credential_manager: new module to manage credentials * fix sanity issues and removed CredSSP references * renamed module to win_credential * fix typo on test variable * fix sanity ignore line
This commit is contained in:
parent
10af3874b5
commit
8e92cca139
10 changed files with 2104 additions and 0 deletions
713
lib/ansible/modules/windows/win_credential.ps1
Normal file
713
lib/ansible/modules/windows/win_credential.ps1
Normal file
|
@ -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<CredentialAttribute> attributes = new List<CredentialAttribute>();
|
||||
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<CredentialAttribute> Attributes = new List<CredentialAttribute>();
|
||||
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<SafeMemoryBuffer> attributeBuffers = new List<SafeMemoryBuffer>();
|
||||
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>(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()
|
||||
|
218
lib/ansible/modules/windows/win_credential.py
Normal file
218
lib/ansible/modules/windows/win_credential.py
Normal file
|
@ -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'''
|
||||
#
|
||||
'''
|
1
test/integration/targets/win_credential/aliases
Normal file
1
test/integration/targets/win_credential/aliases
Normal file
|
@ -0,0 +1 @@
|
|||
shippable/windows/group3
|
19
test/integration/targets/win_credential/defaults/main.yml
Normal file
19
test/integration/targets/win_credential/defaults/main.yml
Normal file
|
@ -0,0 +1,19 @@
|
|||
# The certificate in files/cert.pfx was generated with the following commands
|
||||
#
|
||||
# cat > client.cnf <<EOL
|
||||
# [ssl_client]
|
||||
# basicConstraints = CA:FALSE
|
||||
# nsCertType = client
|
||||
# keyUsage = digitalSignature, keyEncipherment
|
||||
# extendedKeyUsage = clientAuth
|
||||
# EOL
|
||||
#
|
||||
# openssl genrsa -aes256 -passout pass:password1 -out cert.key 2048
|
||||
# openssl req -new -subj '/CN=ansible.domain.com' -key cert.key -out cert.req -passin pass:password1
|
||||
# openssl x509 -sha256 -req -in cert.req -days 24855 -signkey cert.key -out cert.crt -extensions ssl_client -extfile client.cnf -passin pass:password1
|
||||
# openssl pkcs12 -export -in cert.crt -inkey cert.key -out cert.pfx -passin pass:password1 -passout pass:password1
|
||||
---
|
||||
test_credential_dir: '{{ win_output_dir }}\win_credential_manager'
|
||||
test_hostname: ansible.domain.com
|
||||
key_password: password1
|
||||
cert_thumbprint: 56841AAFDD19D7DF474BDA24D01D88BD8025A00A
|
BIN
test/integration/targets/win_credential/files/cert.pfx
Normal file
BIN
test/integration/targets/win_credential/files/cert.pfx
Normal file
Binary file not shown.
|
@ -0,0 +1,498 @@
|
|||
#!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 = @{
|
||||
name = @{ type = "str"; required = $true }
|
||||
type = @{ type = "str"; required = $true; choices = @("domain_password", "domain_certificate", "generic_password", "generic_certificate") }
|
||||
}
|
||||
supports_check_mode = $true
|
||||
}
|
||||
|
||||
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||
|
||||
$name = $module.Params.name
|
||||
$type = $module.Params.type
|
||||
|
||||
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<CredentialAttribute> attributes = new List<CredentialAttribute>();
|
||||
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<CredentialAttribute> Attributes = new List<CredentialAttribute>();
|
||||
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<SafeMemoryBuffer> attributeBuffers = new List<SafeMemoryBuffer>();
|
||||
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>(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()
|
||||
|
2
test/integration/targets/win_credential/meta/main.yml
Normal file
2
test/integration/targets/win_credential/meta/main.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
dependencies:
|
||||
- prepare_win_tests
|
64
test/integration/targets/win_credential/tasks/main.yml
Normal file
64
test/integration/targets/win_credential/tasks/main.yml
Normal file
|
@ -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
|
588
test/integration/targets/win_credential/tasks/tests.yml
Normal file
588
test/integration/targets/win_credential/tasks/tests.yml
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue