1 #!powershell
2
3 # Copyright: (c) 2020, Ansible Project
4 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6 #AnsibleRequires -CSharpUtil Ansible.Basic
7 #Requires -Module Ansible.ModuleUtils.AddType
8
9 $spec = @{
10 options = @{
11 domain_password = @{ type = 'str'; no_log = $true }
12 domain_server = @{ type = 'str' }
13 domain_username = @{ type = 'str' }
=()14 filter = @{ type = 'str' }
15 identity = @{ type = 'str' }
16 include_deleted = @{ type = 'bool'; default = $false }
17 ldap_filter = @{ type = 'str' }
18 properties = @{ type = 'list'; elements = 'str' }
19 search_base = @{ type = 'str' }
20 search_scope = @{ type = 'str'; choices = @('base', 'one_level', 'subtree') }
21 }
22 supports_check_mode = $true
23 mutually_exclusive = @(
24 @('filter', 'identity', 'ldap_filter'),
25 @('identity', 'search_base'),
26 @('identity', 'search_scope')
27 )
28 required_one_of = @(
29 ,@('filter', 'identity', 'ldap_filter')
30 )
31 required_together = @(,@('domain_username', 'domain_password'))
32 }
33
34 $module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
35
36 $module.Result.objects = @() # Always ensure this is returned even in a failure.
37
38 $domainServer = $module.Params.domain_server
39 $domainPassword = $module.Params.domain_password
40 $domainUsername = $module.Params.domain_username
41 $filter = $module.Params.filter
42 $identity = $module.Params.identity
43 $includeDeleted = $module.Params.include_deleted
44 $ldapFilter = $module.Params.ldap_filter
45 $properties = $module.Params.properties
46 $searchBase = $module.Params.search_base
47 $searchScope = $module.Params.search_scope
48
49 $credential = $null
50 if ($domainUsername) {
51 $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @(
52 $domainUsername,
53 (ConvertTo-SecureString -AsPlainText -Force -String $domainPassword)
54 )
55 }
56
57 Add-CSharpType -References @'
58 using System;
59
60 namespace Ansible.WinDomainObjectInfo
61 {
62 [Flags]
63 public enum UserAccountControl : int
64 {
65 ADS_UF_SCRIPT = 0x00000001,
66 ADS_UF_ACCOUNTDISABLE = 0x00000002,
67 ADS_UF_HOMEDIR_REQUIRED = 0x00000008,
68 ADS_UF_LOCKOUT = 0x00000010,
69 ADS_UF_PASSWD_NOTREQD = 0x00000020,
70 ADS_UF_PASSWD_CANT_CHANGE = 0x00000040,
71 ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED = 0x00000080,
72 ADS_UF_TEMP_DUPLICATE_ACCOUNT = 0x00000100,
73 ADS_UF_NORMAL_ACCOUNT = 0x00000200,
74 ADS_UF_INTERDOMAIN_TRUST_ACCOUNT = 0x00000800,
75 ADS_UF_WORKSTATION_TRUST_ACCOUNT = 0x00001000,
76 ADS_UF_SERVER_TRUST_ACCOUNT = 0x00002000,
77 ADS_UF_DONT_EXPIRE_PASSWD = 0x00010000,
78 ADS_UF_MNS_LOGON_ACCOUNT = 0x00020000,
79 ADS_UF_SMARTCARD_REQUIRED = 0x00040000,
80 ADS_UF_TRUSTED_FOR_DELEGATION = 0x00080000,
81 ADS_UF_NOT_DELEGATED = 0x00100000,
82 ADS_UF_USE_DES_KEY_ONLY = 0x00200000,
83 ADS_UF_DONT_REQUIRE_PREAUTH = 0x00400000,
84 ADS_UF_PASSWORD_EXPIRED = 0x00800000,
85 ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0x01000000,
86 }
87
88 public enum sAMAccountType : int
89 {
90 SAM_DOMAIN_OBJECT = 0x00000000,
91 SAM_GROUP_OBJECT = 0x10000000,
92 SAM_NON_SECURITY_GROUP_OBJECT = 0x10000001,
93 SAM_ALIAS_OBJECT = 0x20000000,
94 SAM_NON_SECURITY_ALIAS_OBJECT = 0x20000001,
95 SAM_USER_OBJECT = 0x30000000,
96 SAM_NORMAL_USER_ACCOUNT = 0x30000000,
97 SAM_MACHINE_ACCOUNT = 0x30000001,
98 SAM_TRUST_ACCOUNT = 0x30000002,
99 SAM_APP_BASIC_GROUP = 0x40000000,
100 SAM_APP_QUERY_GROUP = 0x40000001,
101 SAM_ACCOUNT_TYPE_MAX = 0x7fffffff,
102 }
103 }
104 '@
105
ConvertTo-OutputValuenull106 Function ConvertTo-OutputValue {
107 [CmdletBinding()]
108 Param (
109 [Parameter(Mandatory=$true)]
110 [AllowNull()]
111 [Object]
112 $InputObject
113 )
114
115 if ($InputObject -is [System.Security.Principal.SecurityIdentifier]) {
116 # Syntax: SID - Only serialize the SID as a string and not the other metadata properties.
117 $sidInfo = @{
118 Sid = $InputObject.Value
119 }
120
121 # Try and map the SID to the account name, this may fail if the SID is invalid or not mappable.
122 try {
123 $sidInfo.Name = $InputObject.Translate([System.Security.Principal.NTAccount]).Value
124 } catch [System.Security.Principal.IdentityNotMappedException] {
125 $sidInfo.Name = $null
126 }
127
128 $sidInfo
129 } elseif ($InputObject -is [Byte[]]) {
130 # Syntax: Octet String - By default will serialize as a list of decimal values per byte, instead return a
131 # Base64 string as Ansible can easily parse that.
132 [System.Convert]::ToBase64String($InputObject)
133 } elseif ($InputObject -is [DateTime]) {
134 # Syntax: UTC Coded Time - .NET DateTimes serialized as in the form "Date(FILETIME)" which isn't easily
135 # parsable by Ansible, instead return as an ISO 8601 string in the UTC timezone.
136 [TimeZoneInfo]::ConvertTimeToUtc($InputObject).ToString("o")
137 } elseif ($InputObject -is [System.Security.AccessControl.ObjectSecurity]) {
138 # Complex object which isn't easily serializable. Instead we should just return the SDDL string. If a user
139 # needs to parse this then they really need to reprocess the SDDL string and process their results on another
140 # win_shell task.
141 $InputObject.GetSecurityDescriptorSddlForm(([System.Security.AccessControl.AccessControlSections]::All))
142 } else {
143 # Syntax: (All Others) - The default serialization handling of other syntaxes are fine, don't do anything.
144 $InputObject
145 }
146 }
147
148 <#
149 Calling Get-ADObject that returns multiple objects with -Properties * will only return the properties that were set on
150 the first found object. To counter this problem we will first call Get-ADObject to list all the objects that match the
specified()151 filter specified then get the properties on each object.
152 #>
153
154 $commonParams = @{
155 IncludeDeletedObjects = $includeDeleted
156 }
157
158 if ($credential) {
159 $commonParams.Credential = $credential
160 }
161
162 if ($domainServer) {
163 $commonParams.Server = $domainServer
164 }
165
166 # First get the IDs for all the AD objects that match the filter specified.
167 $getParams = @{
168 Properties = @('DistinguishedName', 'ObjectGUID')
169 }
170
171 if ($filter) {
172 $getParams.Filter = $filter
173 } elseif ($identity) {
174 $getParams.Identity = $identity
175 } elseif ($ldapFilter) {
176 $getParams.LDAPFilter = $ldapFilter
177 }
178
179 # Explicit check on $null as an empty string is different from not being set.
180 if ($null -ne $searchBase) {
181 $getParams.SearchBase = $searchbase
182 }
183
184 if ($searchScope) {
185 $getParams.SearchScope = switch($searchScope) {
186 base { 'Base' }
187 one_level { 'OneLevel' }
188 subtree { 'Subtree' }
189 }
190 }
191
192 try {
193 # We run this in a custom PowerShell pipeline so that users of this module can't use any of the variables defined
194 # above in their filter. While the cmdlet won't execute sub expressions we don't want anyone implicitly relying on
195 # a defined variable in this module in case we ever change the name or remove it.
196 $ps = [PowerShell]::Create()
197 $null = $ps.AddCommand('Get-ADObject').AddParameters($commonParams).AddParameters($getParams)
198 $null = $ps.AddCommand('Select-Object').AddParameter('Property', @('DistinguishedName', 'ObjectGUID'))
199
200 $foundGuids = @($ps.Invoke())
201 } catch {
202 # Because we ran in a pipeline we can't catch ADIdentityNotFoundException. Instead just get the base exception and
203 # do the error checking on that.
204 if ($_.Exception.GetBaseException() -is [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]) {
205 $foundGuids = @()
206 } else {
207 # The exception is from the .Invoke() call, compare on the InnerException which was what was actually raised by
208 # the pipeline.
209 $innerException = $_.Exception.InnerException.InnerException
210 if ($innerException -is [Microsoft.ActiveDirectory.Management.ADServerDownException]) {
211 # Point users in the direction of the double hop problem as that is what is typically the cause of this.
212 $msg = "Failed to contact the AD server, this could be caused by the double hop problem over WinRM. "
213 $msg += "Try using the module with auth as Kerberos with credential delegation or CredSSP, become, or "
214 $msg += "defining the domain_username and domain_password module parameters."
215 $module.FailJson($msg, $innerException)
216 } else {
217 throw $innerException
218 }
219 }
220 }
221
222 $getParams = @{}
223 if ($properties) {
224 $getParams.Properties = $properties
225 }
226 $module.Result.objects = @(foreach ($adId in $foundGuids) {
227 try {
228 $adObject = Get-ADObject @commonParams @getParams -Identity $adId.ObjectGUID
229 } catch {
230 $msg = "Failed to retrieve properties for AD Object '$($adId.DistinguishedName)': $($_.Exception.Message)"
231 $module.Warn($msg)
232 continue
233 }
234
235 $propertyNames = $adObject.PropertyNames
236 $propertyNames += ($properties | Where-Object { $_ -ne '*' })
237
238 # Now process each property to an easy to represent string
239 $filteredObject = [Ordered]@{}
240 foreach ($name in ($propertyNames | Sort-Object)) {
241 # In the case of explicit properties that were asked for but weren't set, Get-ADObject won't actually return
242 # the property so this is a defensive check against that scenario.
243 if (-not $adObject.PSObject.Properties.Name.Contains($name)) {
244 $filteredObject.$name = $null
245 continue
246 }
247
248 $value = $adObject.$name
249 if ($value -is [Microsoft.ActiveDirectory.Management.ADPropertyValueCollection]) {
250 $value = foreach ($v in $value) {
251 ConvertTo-OutputValue -InputObject $v
252 }
253 } else {
254 $value = ConvertTo-OutputValue -InputObject $value
255 }
256 $filteredObject.$name = $value
257
258 # For these 2 properties, add an _AnsibleFlags attribute which contains the enum strings that are set.
259 if ($name -eq 'sAMAccountType') {
260 $enumValue = [Ansible.WinDomainObjectInfo.sAMAccountType]$value
261 $filteredObject.'sAMAccountType_AnsibleFlags' = $enumValue.ToString() -split ', '
262 } elseif ($name -eq 'userAccountControl') {
263 $enumValue = [Ansible.WinDomainObjectInfo.UserAccountControl]$value
264 $filteredObject.'userAccountControl_AnsibleFlags' = $enumValue.ToString() -split ', '
265 }
266 }
267
268 $filteredObject
269 })
270
271 $module.ExitJson()
272