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
|
#
# 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"
require "uuidtools"
require "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"
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 mac? && node["platform_version"] =~ "> 10.15"
raise "The osx_profile resource is not available on macOS Bug Sur or above due to the removal of apple support for CLI installation of profiles"
end
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
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)
if update
node.run_state[:config_profiles] = query_installed_profiles
else
node.run_state[:config_profiles] ||= query_installed_profiles
end
logger.trace("Saved profiles to run_state")
end
def query_installed_profiles
Tempfile.open("allprofiles.plist") do |tempfile|
shell_out!( "/usr/bin/profiles", "-P", "-o", tempfile.path )
::Plist.parse_xml(tempfile)
end
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
|