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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
|
#
# Author:: Nate Walck (<nate.walck@gmail.com>)
# Copyright:: Copyright 2015-2016, Facebook, Inc.
# License:: Apache License, Version 2.0
#
# 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_relative "../log"
require_relative "../resource/file"
autoload :UUIDTools, "uuidtools"
autoload :Plist, "plist"
class Chef
class Resource
class OsxProfile < Chef::Resource
unified_mode true
provides :osx_profile
provides :osx_config_profile
description "Use the **osx_profile** resource to manage configuration profiles (`.mobileconfig` files) on the macOS platform. The **osx_profile** resource installs profiles by using the uuidgen library to generate a unique `ProfileUUID`, and then using the `profiles` command to install the profile on the system."
introduced "12.7"
examples <<~DOC
**Install a profile from a cookbook file**
```ruby
osx_profile 'com.company.screensaver.mobileconfig'
```
**Install profile from a hash**
```ruby
profile_hash = {
'PayloadIdentifier' => 'com.company.screensaver',
'PayloadRemovalDisallowed' => false,
'PayloadScope' => 'System',
'PayloadType' => 'Configuration',
'PayloadUUID' => '1781fbec-3325-565f-9022-8aa28135c3cc',
'PayloadOrganization' => 'Chef',
'PayloadVersion' => 1,
'PayloadDisplayName' => 'Screensaver Settings',
'PayloadContent'=> [
{
'PayloadType' => 'com.apple.ManagedClient.preferences',
'PayloadVersion' => 1,
'PayloadIdentifier' => 'com.company.screensaver',
'PayloadUUID' => '73fc30e0-1e57-0131-c32d-000c2944c108',
'PayloadEnabled' => true,
'PayloadDisplayName' => 'com.apple.screensaver',
'PayloadContent' => {
'com.apple.screensaver' => {
'Forced' => [
{
'mcx_preference_settings' => {
'idleTime' => 0,
}
}
]
}
}
}
]
}
osx_profile 'Install screensaver profile' do
profile profile_hash
end
```
**Remove profile using identifier in resource name**
```ruby
osx_profile 'com.company.screensaver' do
action :remove
end
```
**Remove profile by identifier and user friendly resource name**
```ruby
osx_profile 'Remove screensaver profile' do
identifier 'com.company.screensaver'
action :remove
end
```
DOC
property :profile_name, String,
description: "Use to specify the name of the profile, if different from the name of the resource block.",
name_property: true
property :profile, [ String, Hash ],
description: "Use to specify a profile. This may be the name of a profile contained in a cookbook or a Hash that contains the contents of the profile."
property :identifier, String,
description: "Use to specify the identifier for the profile, such as `com.company.screensaver`."
# this is not a property it is necessary for the tempfile this resource uses to work (FIXME: this is terrible)
#
# @api private
#
def path(path = nil)
@path ||= path
@path
end
action_class do
def load_current_resource
@current_resource = Chef::Resource::OsxProfile.new(new_resource.name)
current_resource.profile_name(new_resource.profile_name)
if new_profile_hash
new_profile_hash["PayloadUUID"] = config_uuid(new_profile_hash)
end
current_resource.profile(current_profile)
end
def current_profile
all_profiles = get_installed_profiles
if all_profiles && all_profiles.key?("_computerlevel")
return all_profiles["_computerlevel"].find do |item|
item["ProfileIdentifier"] == new_profile_identifier
end
end
nil
end
def invalid_profile_name?(name_or_identifier)
name_or_identifier.end_with?(".mobileconfig") || !/^\w+(?:(\.| )\w+)+$/.match(name_or_identifier)
end
def check_resource_semantics!
if action == :remove
if new_profile_identifier
if invalid_profile_name?(new_profile_identifier)
raise "when removing using the identifier property, it must match the profile identifier"
end
else
if invalid_profile_name?(new_resource.profile_name)
raise "When removing by resource name, it must match the profile identifier"
end
end
end
if action == :install
# we only do this check for the install action so that profiles can still be removed on macOS 11+
if mac? && node["platform_version"] =~ ">= 11.0"
raise "The osx_profile resource is not available on macOS Big Sur or above due to Apple's removal of support for CLI profile installation"
end
if new_profile_hash.is_a?(Hash) && !new_profile_hash.include?("PayloadIdentifier")
raise "The specified profile does not seem to be valid"
end
if new_profile_hash.is_a?(String) && !new_profile_hash.end_with?(".mobileconfig")
raise "#{new_profile_hash}' is not a valid profile"
end
end
end
end
action :install do
unless profile_installed?
converge_by("install profile #{new_profile_identifier}") do
profile_path = write_profile_to_disk
install_profile(profile_path)
get_installed_profiles(true)
end
end
end
action :remove do
# Clean up profile after removing it
if profile_installed?
converge_by("remove profile #{new_profile_identifier}") do
remove_profile
get_installed_profiles(true)
end
end
end
action_class do
private
def profile
@profile ||= new_resource.profile || new_resource.profile_name
end
def new_profile_hash
@new_profile_hash ||= get_profile_hash(profile)
end
def new_profile_identifier
@new_profile_identifier ||= if new_profile_hash
new_profile_hash["PayloadIdentifier"]
else
new_resource.identifier || new_resource.profile_name
end
end
def load_profile_hash(new_profile)
# file must exist in cookbook
return nil unless new_profile.end_with?(".mobileconfig")
unless cookbook_file_available?(new_profile)
raise Chef::Exceptions::FileNotFound, "#{self}: '#{new_profile}' not found in cookbook"
end
cookbook_profile = cache_cookbook_profile(new_profile)
::Plist.parse_xml(cookbook_profile)
end
def cookbook_file_available?(cookbook_file)
run_context.has_cookbook_file_in_cookbook?(
new_resource.cookbook_name, cookbook_file
)
end
def get_cache_dir
Chef::FileCache.create_cache_path(
"profiles/#{new_resource.cookbook_name}"
)
end
def cache_cookbook_profile(cookbook_file)
Chef::FileCache.create_cache_path(
::File.join(
"profiles",
new_resource.cookbook_name,
::File.dirname(cookbook_file)
)
)
path = ::File.join( get_cache_dir, "#{cookbook_file}.remote")
cookbook_file path do
cookbook_name = new_resource.cookbook_name
source(cookbook_file)
backup(false)
run_action(:create)
end
path
end
def get_profile_hash(new_profile)
if new_profile.is_a?(Hash)
new_profile
elsif new_profile.is_a?(String)
load_profile_hash(new_profile)
end
end
def config_uuid(profile)
# Make a UUID of the profile contents and return as string
UUIDTools::UUID.sha1_create(
UUIDTools::UUID_DNS_NAMESPACE,
profile.to_s
).to_s
end
def write_profile_to_disk
# FIXME: this is kind of terrible, the resource needs a tempfile to use and
# wants it created similarly to the file providers (with all the magic necessary
# for determining if it should go in the cwd or into a tmpdir), but it abuses
# the Chef::FileContentManagement::Tempfile API to do that, which requires setting
# a `path` method on the resource because of tight-coupling to the file provider
# pattern. We don't just want to use a file here because the point is to get
# at the tempfile pattern from the file provider, but to feed that into a shell
# command rather than deploying the file to somewhere on disk. There's some
# better API that needs extracting here.
new_resource.path(Chef::FileCache.create_cache_path("profiles"))
tempfile = Chef::FileContentManagement::Tempfile.new(new_resource).tempfile
tempfile.write(new_profile_hash.to_plist)
tempfile.close
tempfile.path
end
def install_profile(profile_path)
cmd = [ "/usr/bin/profiles", "-I", "-F", profile_path ]
logger.trace("cmd: #{cmd.join(" ")}")
shell_out!(*cmd)
end
def remove_profile
cmd = [ "/usr/bin/profiles", "-R", "-p", new_profile_identifier ]
logger.trace("cmd: #{cmd.join(" ")}")
shell_out!(*cmd)
end
#
# FIXME FIXME FIXME
# The node object should not be used for caching state like this and this is not a public API and may break.
# FIXME FIXME FIXME
#
def get_installed_profiles(update = nil)
logger.trace("Saving profile data to node.run_state")
if update
node.run_state[:config_profiles] = query_installed_profiles
else
node.run_state[:config_profiles] ||= query_installed_profiles
end
end
def query_installed_profiles
logger.trace("Running /usr/bin/profiles -P -o stdout-xml to determine profile state")
so = shell_out( "/usr/bin/profiles", "-P", "-o", "stdout-xml" )
::Plist.parse_xml(so.stdout)
end
def profile_installed?
# Profile Identifier and UUID must match a currently installed profile
return false if current_resource.profile.nil? || current_resource.profile.empty?
return true if action == :remove
current_resource.profile["ProfileUUID"] == new_profile_hash["PayloadUUID"]
end
end
end
end
end
|