summaryrefslogtreecommitdiff
path: root/lib/chef/resource/windows_ad_join.rb
blob: 26da5fb725646bd8025f000f35a80950ac9baef0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
#
# Author:: John Snow (<jsnow@chef.io>)
# Copyright:: 2016-2018, John Snow
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require_relative "../resource"
require "chef-utils/dist" unless defined?(ChefUtils::Dist)

class Chef
  class Resource
    class WindowsAdJoin < Chef::Resource
      provides :windows_ad_join

      unified_mode true

      description "Use the **windows_ad_join** resource to join a Windows Active Directory domain."
      introduced "14.0"
      examples <<~DOC
        **Join a domain**

        ```ruby
        windows_ad_join 'ad.example.org' do
          domain_user 'nick'
          domain_password 'p@ssw0rd1'
        end
        ```

        **Join a domain, as `win-workstation`**

        ```ruby
        windows_ad_join 'ad.example.org' do
          domain_user 'nick'
          domain_password 'p@ssw0rd1'
          new_hostname 'win-workstation'
        end
        ```

        **Leave the current domain and re-join the `local` workgroup**

        ```ruby
        windows_ad_join 'Leave domain' do
          action :leave
          workgroup 'local'
        end
        ```
      DOC

      property :domain_name, String,
        description: "An optional property to set the FQDN of the Active Directory domain to join if it differs from the resource block's name.",
        validation_message: "The 'domain_name' property must be a FQDN.",
        regex: /.\../, # anything.anything
        name_property: true

      property :domain_user, String,
        description: "The domain user that will be used to join the domain.",
        required: true

      property :domain_password, String,
        description: "The password for the domain user. Note that this resource is set to hide sensitive information by default. ",
        required: true

      property :ou_path, String,
        description: "The path to the Organizational Unit where the host will be placed."

      property :reboot, Symbol,
        equal_to: %i{immediate delayed never request_reboot reboot_now},
        validation_message: "The reboot property accepts :immediate (reboot as soon as the resource completes), :delayed (reboot once the #{ChefUtils::Dist::Infra::PRODUCT} run completes), and :never (Don't reboot)",
        description: "Controls the system reboot behavior post domain joining. Reboot immediately, after the #{ChefUtils::Dist::Infra::PRODUCT} run completes, or never. Note that a reboot is necessary for changes to take effect.",
        default: :immediate

      property :reboot_delay, Integer,
        description: "The amount of time (in minutes) to delay a reboot request.",
        default: 0,
        introduced: "16.5"

      property :new_hostname, String,
        description: "Specifies a new hostname for the computer in the new domain.",
        introduced: "14.5"

      property :workgroup_name, String,
        description: "Specifies the name of a workgroup to which the computer is added to when it is removed from the domain. The default value is WORKGROUP. This property is only applicable to the :leave action.",
        introduced: "15.4"

      # define this again so we can default it to true. Otherwise failures print the password
      property :sensitive, [TrueClass, FalseClass],
        default: true, desired_state: false

      action :join, description: "Join the Active Directory domain." do
        unless on_desired_domain?
          cmd = "$pswd = ConvertTo-SecureString \'#{new_resource.domain_password}\' -AsPlainText -Force;"
          cmd << "$credential = New-Object System.Management.Automation.PSCredential (\"#{sanitize_usename}\",$pswd);"
          cmd << "Add-Computer -DomainName #{new_resource.domain_name} -Credential $credential"
          cmd << " -OUPath \"#{new_resource.ou_path}\"" if new_resource.ou_path
          cmd << " -NewName \"#{new_resource.new_hostname}\"" if new_resource.new_hostname
          cmd << " -Force"

          converge_by("join Active Directory domain #{new_resource.domain_name}") do
            ps_run = powershell_exec(cmd)
            if ps_run.error?
              if sensitive?
                raise "Failed to join the domain #{new_resource.domain_name}: *suppressed sensitive resource output*"
              else
                raise "Failed to join the domain #{new_resource.domain_name}: #{ps_run.errors}"
              end
            end

            unless new_resource.reboot == :never
              reboot "Reboot to join domain #{new_resource.domain_name}" do
                action clarify_reboot(new_resource.reboot)
                delay_mins new_resource.reboot_delay
                reason "Reboot to join domain #{new_resource.domain_name}"
              end
            end
          end
        end
      end

      action :leave, description: "Leave an Active Directory domain and re-join a workgroup." do
        if joined_to_domain?
          cmd = ""
          cmd << "$pswd = ConvertTo-SecureString \'#{new_resource.domain_password}\' -AsPlainText -Force;"
          cmd << "$credential = New-Object System.Management.Automation.PSCredential (\"#{sanitize_usename}\",$pswd);"
          cmd << "Remove-Computer"
          cmd << " -UnjoinDomainCredential $credential"
          cmd << " -NewName \"#{new_resource.new_hostname}\"" if new_resource.new_hostname
          cmd << " -WorkgroupName \"#{new_resource.workgroup_name}\"" if new_resource.workgroup_name
          cmd << " -Force"

          converge_by("leave Active Directory domain #{node_domain}") do
            ps_run = powershell_exec(cmd)
            if ps_run.error?
              if sensitive?
                raise "Failed to leave the domain #{node_domain}: *suppressed sensitive resource output*"
              else
                raise "Failed to leave the domain #{node_domain}: #{ps_run.errors}"
              end
            end

            unless new_resource.reboot == :never
              reboot "Reboot to leave domain #{new_resource.domain_name}" do
                action clarify_reboot(new_resource.reboot)
                delay_mins new_resource.reboot_delay
                reason "Reboot to leave domain #{new_resource.domain_name}"
              end
            end
          end
        end
      end

      action_class do
        #
        # @return [String] The domain name the node is joined to. When the node
        #   is not joined to a domain this will return the name of the
        #   workgroup the node is a member of.
        #
        def node_domain
          node_domain = powershell_exec!("(Get-WmiObject Win32_ComputerSystem).Domain")
          raise "Failed to check if the system is joined to the domain #{new_resource.domain_name}: #{node_domain.errors}}" if node_domain.error?

          node_domain.result.downcase.strip
        end

        #
        # @return [String] The workgroup the node is a member of. This will
        #   return an empty string if the system is not a member of a
        #   workgroup.
        #
        def node_workgroup
          node_workgroup = powershell_exec!("(Get-WmiObject Win32_ComputerSystem).Workgroup")
          raise "Failed to check if the system is currently a member of a workgroup" if node_workgroup.error?

          node_workgroup.result
        end

        #
        # @return [true, false] Whether or not the node is joined to ANY domain
        #
        def joined_to_domain?
          node_workgroup.empty? && !node_domain.empty?
        end

        #
        # @return [true, false] Whether or not the node is joined to the domain
        #   defined by the resource :domain_name property.
        #
        def on_desired_domain?
          node_domain == new_resource.domain_name.downcase
        end

        #
        # @return [String] the correct user and domain to use.
        #   if the domain_user property contains an @ symbol followed by any number of non white space characters
        #   then we assume it is a user from another domain than the one specified in the resource domain_name property.
        #   if this is the case we do not append the domain_name property to the domain_user property
        #   the domain_user and domain_name form the UPN (userPrincipalName)
        #   The specification for the UPN format is RFC 822
        #   links: https://docs.microsoft.com/en-us/windows/win32/ad/naming-properties#userprincipalname https://tools.ietf.org/html/rfc822
        #   regex: https://rubular.com/r/isAWojpTMKzlnp
        def sanitize_usename
          if /@/.match?(new_resource.domain_user)
            new_resource.domain_user
          else
            "#{new_resource.domain_user}@#{new_resource.domain_name}"
          end
        end

        # This resource historically took `:immediate` and `:delayed` as arguments to the reboot property but then
        # tried to shove that straight to the `reboot` resource which objected strenuously
        def clarify_reboot(reboot_action)
          case reboot_action
          when :immediate
            :reboot_now
          when :delayed
            :request_reboot
          else
            reboot_action
          end
        end

        def sensitive?
          !!new_resource.sensitive
        end
      end
    end
  end
end