1
0
Fork 0
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:
Jordan Borean 2018-11-22 06:55:48 +10:00 committed by GitHub
parent 10af3874b5
commit 8e92cca139
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 2104 additions and 0 deletions

View 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()

View 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'''
#
'''

View file

@ -0,0 +1 @@
shippable/windows/group3

View 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

Binary file not shown.

View file

@ -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()

View file

@ -0,0 +1,2 @@
dependencies:
- prepare_win_tests

View 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

View 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

View file

@ -12,6 +12,7 @@ lib/ansible/modules/windows/setup.ps1 PSAvoidUsingEmptyCatchBlock
lib/ansible/modules/windows/setup.ps1 PSUseDeclaredVarsMoreThanAssignments lib/ansible/modules/windows/setup.ps1 PSUseDeclaredVarsMoreThanAssignments
lib/ansible/modules/windows/win_copy.ps1 PSUseApprovedVerbs lib/ansible/modules/windows/win_copy.ps1 PSUseApprovedVerbs
lib/ansible/modules/windows/win_copy.ps1 PSUseDeclaredVarsMoreThanAssignments 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 PSAvoidGlobalVars
lib/ansible/modules/windows/win_dns_client.ps1 PSAvoidUsingCmdletAliases lib/ansible/modules/windows/win_dns_client.ps1 PSAvoidUsingCmdletAliases
lib/ansible/modules/windows/win_dns_client.ps1 PSAvoidUsingWMICmdlet lib/ansible/modules/windows/win_dns_client.ps1 PSAvoidUsingWMICmdlet