Class: Puppet::Provider::MobileConfig
- Inherits:
-
Puppet::Provider
- Object
- Puppet::Provider
- Puppet::Provider::MobileConfig
- Defined in:
- lib/puppet/provider/mobileconfig.rb
Class Method Summary collapse
-
.fetch_resources(scrub_uuids = false) ⇒ Object
Rather than letting #instances control what a resource looks like, def a method to collect the data.
-
.get_installed_profiles ⇒ Object
Use the profiles command to return an array containing a Hash representation of each of the profiles installed Returns: Array.
-
.get_resource_properties(profile) ⇒ Object
Profile read from profile dump goes in, Puppet resource comes out.
-
.instances ⇒ Object
Returns an Array of a provider instances for every resource discovered.
-
.parse_propertylist(file) ⇒ Object
Parse a plist and return a Ruby object.
-
.prefetch(resources) ⇒ Object
Puppet MAGIC.
-
.prepare_content(content) ⇒ Object
Formats the PayloadContent data for use the in the resource.
Instance Method Summary collapse
-
#add_activedirectory_keys(payload) ⇒ Object
This used to be accomplished in the :activedirectory provider, but we are collapsing this functionality back into the parent class and abandoning the sub-classed provider.
-
#coalesce_mobileconfig ⇒ Object
Provider Helper method Build and install the mobileconfig OR destroy it.
- #create ⇒ Object
- #destroy ⇒ Object
- #exists? ⇒ Boolean
-
#flush ⇒ Object
Puppet MAGIC The flush method is called once per resource whenever the ‘is’ and ‘should’ values for a property differ (and synchronization needs to occur).
-
#initialize(value = {}) ⇒ MobileConfig
constructor
A new instance of MobileConfig.
-
#parse_cert_data_from_blob(blob) ⇒ Object
Validates a Blob as a com.apple.security.pkcs1 Certificate Payload.
-
#parse_cert_data_from_string(string) ⇒ Object
Validates a String as a com.apple.security.pkcs1 Certificate Payload - will decode Base64 if it can.
-
#process_certificate_payload(payload) ⇒ Object
Processes the com.apple.security.pkcs1 PayloadContent as required.
-
#transform_content(content) ⇒ Object
Formats and fortifies the PayloadContent array - ensures required keys to each Hash.
Constructor Details
#initialize(value = {}) ⇒ MobileConfig
Returns a new instance of MobileConfig.
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 |
# File 'lib/puppet/provider/mobileconfig.rb', line 134 def initialize(value={}) super(value) @property_flush = {} # A little Ruby metaprogramming magic... # # Insert a singleton method after intialization that overrides the #content # getter so that we can intercept any Password key/values. # # To enable this to work with the `mk_resource_methods` method (a class # method which we like for its convenience) the content method must be # fasionably late to the party. So, use the define_singleton_method after # super init to ensure our method isn't squashed. # # Why do we need this method override? # # Certain PayloadTypes contain information that gets scrubbed from the # `/usr/bin/profiles` output. In particular, these: # - com.apple.wifi.managed # - com.apple.firstactiveethernet.managed # - com.apple.DirectoryService.managed # # All of these PayloadTypes use a Password key whose value is output as: # '********' -- negating Puppet's ability to compare it with the content # specified in the resource declaration. # # As a workaround, we perform a substitution, re-inserting the specified # value. This is sub-optimal and means that this value is # NOT IDEMPOTENT (ie. changes to this value, will not trigger a puppet # apply). # # However, there is a workaround if you follow this rule of thumb: # # If you change the password, rotate/change/set the PayloadUUID inside # the affected Payload. # # Doing this will signal Puppet that something has changed and it will # reinstall the profile. define_singleton_method(:content) do if @resource[:content] and not @resource[:content].empty? return @property_hash[:content].each_with_index.map do |hash, i| if hash.key?('Password') hash['Password'] = @resource[:content][i]['Password'] end hash end end @property_hash[:content] || :absent end end |
Class Method Details
.fetch_resources(scrub_uuids = false) ⇒ Object
Rather than letting #instances control what a resource looks like, def a method to collect the data. This way we can choose whterh or not to scrub the PayloadUUID which we now use as a checksum.
29 30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'lib/puppet/provider/mobileconfig.rb', line 29 def fetch_resources(scrub_uuids=false) all = get_installed_profiles all.collect do |profile| resource = get_resource_properties(profile) if scrub_uuids resource[:content].collect! do |hash| hash.delete('PayloadUUID') hash end end new(resource) end end |
.get_installed_profiles ⇒ Object
Use the profiles command to return an array containing a Hash representation of each of the profiles installed Returns: Array
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/puppet/provider/mobileconfig.rb', line 46 def get_installed_profiles # Setup a tmp dir we can dump the installed profiles in dir = Dir.mktmpdir path = [dir, "profiles#{SecureRandom.hex}.plist"].join("/") begin profiles(['-P', '-o', path]) rescue Puppet::ExecutionFailure => e raise Puppet::Error, "#mobileconfig: command returned non-zero `profiles -P -o #{path}`" end # Parse the plist and remove it parsed = parse_propertylist path FileUtils.rm_rf path # Return an empty array if there are no profiles installed return [] if parsed.empty? parsed['_computerlevel'] end |
.get_resource_properties(profile) ⇒ Object
Profile read from profile dump goes in, Puppet resource comes out
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 |
# File 'lib/puppet/provider/mobileconfig.rb', line 69 def get_resource_properties(profile) # No profile, empty Hash return {} if profile.nil? # Adjust for a key change in Yosemite as per # https://github.com/dayglojesus/managedmac/issues/21 # They changed this key to match what it would be if it # were in a Payload. Yay, parity? removal_disallowed_key = if profile['ProfileUninstallPolicy'] # Mavericks profile['ProfileUninstallPolicy'] == 'allowed' ? 'false' : 'true' else # Yosemite profile['ProfileRemovalDisallowed'] end # Prepare the content array for insertion into the resource content = prepare_content(profile['ProfileItems']) # Ladies and gentleman, the Puppet resource as a Hash { :name => profile['ProfileIdentifier'], :description => profile['ProfileDescription'], :displayname => profile['ProfileDisplayName'], :organization => profile['ProfileOrganization'], :removaldisallowed => removal_disallowed_key, :provider => :mobileconfig, :ensure => :present, :content => content, } end |
.instances ⇒ Object
Returns an Array of a provider instances for every resource discovered
13 14 15 |
# File 'lib/puppet/provider/mobileconfig.rb', line 13 def instances fetch_resources(true) end |
.parse_propertylist(file) ⇒ Object
Parse a plist and return a Ruby object
126 127 128 129 130 |
# File 'lib/puppet/provider/mobileconfig.rb', line 126 def parse_propertylist(file) plist = CFPropertyList::List.new(:file => file) raise Puppet::Error, "Cannot parse: #{file}" if plist.nil? CFPropertyList.native_types(plist.value) end |
.prefetch(resources) ⇒ Object
Puppet MAGIC
18 19 20 21 22 23 24 |
# File 'lib/puppet/provider/mobileconfig.rb', line 18 def prefetch(resources) fetch_resources.each do |prov| if resource = resources[prov.name] resource.provider = prov end end end |
.prepare_content(content) ⇒ Object
Formats the PayloadContent data for use the in the resource
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/puppet/provider/mobileconfig.rb', line 103 def prepare_content(content) content.collect do |item| # Extract the PayloadContent settings = item.delete('PayloadContent') # Scrub the filtered keys ::ManagedMacCommon::FILTERED_PAYLOAD_KEYS.each do |key| item.delete_if { |k| k.eql?(key) } settings.delete_if { |k| k.eql?(key) } end # Reject AD Flag keys # We do this here so the `puppet resource mobileconfig ...` will return # the correct :content no matter which provider is used. This feels # cheap, but it is economical and may be required by other subclasses # of the mobielconfig provider down the road. settings.reject! { |k| k =~ /\AAD.*Flag\z/ } item.merge settings end end |
Instance Method Details
#add_activedirectory_keys(payload) ⇒ Object
This used to be accomplished in the :activedirectory provider, but we are collapsing this functionality back into the parent class and abandoning the sub-classed provider.
The Advanced Active Directory profile contains flag keys which inform the installation process which configuration keys should actually be activated.
support.apple.com/kb/HT5981?viewlocale=en_US&locale=en_US
For example, if we wanted to change the default shell for AD accounts, we would actually need to define two keys: a configuration key and a flag key.
<key>ADDefaultUserShell</key> <string>/bin/zsh</string>
<key>ADDefaultUserShellFlag</key> <true/>
If you fail to specify this second key (the activation or “flag” key), the configuration key will be ignored when the mobileconfig is processed.
To avoid having to activate and deactivate the configuration keys, we pre-process the content array by overriding the transform_content method and shoehorn these flag keys into place dynamically, as required.
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/puppet/provider/mobileconfig.rb', line 225 def add_activedirectory_keys(payload) needs_flag = ['ADAllowMultiDomainAuth', 'ADCreateMobileAccountAtLogin', 'ADDefaultUserShell', 'ADDomainAdminGroupList', 'ADForceHomeLocal', 'ADNamespace', 'ADPacketEncrypt', 'ADPacketSign', 'ADPreferredDCServer', 'ADRestrictDDNS', 'ADTrustChangePassIntervalDays', 'ADUseWindowsUNCPath', 'ADWarnUserBeforeCreatingMA',] needs_flag.each do |e| if payload.key?(e) flag_key = e + 'Flag' payload[flag_key] = true end end payload end |
#coalesce_mobileconfig ⇒ Object
Provider Helper method Build and install the mobileconfig OR destroy it
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 |
# File 'lib/puppet/provider/mobileconfig.rb', line 337 def coalesce_mobileconfig if @property_flush[:ensure] == :absent # Remove the profile id = @resource[:name] begin profiles(['-R', '-p', id]) rescue Puppet::ExecutionFailure => e raise Puppet::Error, "#mobileconfig: command returned non-zero `profiles -R -p #{id}`" end else # Create a tmp dir we can use to house the .mobileconfig path = [Dir.mktmpdir, "#{SecureRandom.hex}.mobileconfig"].join("/") # Transform @resource into usable Hash document = { 'PayloadIdentifier' => @resource[:name], 'PayloadDescription' => @resource[:description], 'PayloadDisplayName' => @resource[:displayname], 'PayloadOrganization' => @resource[:organization], 'PayloadRemovalDisallowed' => @resource[:removaldisallowed] == :false ? false : true, 'PayloadScope' => 'System', 'PayloadType' => 'Configuration', 'PayloadUUID' => SecureRandom.uuid, 'PayloadVersion' => 1, 'PayloadContent' => transform_content(@resource[:content]), } # Parse the document Hash and create new plist file plist = CFPropertyList::List.new plist.value = CFPropertyList.guess(document) plist.save(path, CFPropertyList::List::FORMAT_XML) begin profiles(['-I', '-F', path]) rescue Puppet::ExecutionFailure => e raise Puppet::Error, "#mobileconfig: command returned non-zero `profiles -I -F #{path}`" end FileUtils.rm_rf path if File.exists? path end end |
#create ⇒ Object
187 188 189 |
# File 'lib/puppet/provider/mobileconfig.rb', line 187 def create @property_flush[:ensure] = :present end |
#destroy ⇒ Object
191 192 193 |
# File 'lib/puppet/provider/mobileconfig.rb', line 191 def destroy @property_flush[:ensure] = :absent end |
#exists? ⇒ Boolean
195 196 197 |
# File 'lib/puppet/provider/mobileconfig.rb', line 195 def exists? @property_hash[:ensure] == :present end |
#flush ⇒ Object
Puppet MAGIC The flush method is called once per resource whenever the ‘is’ and ‘should’ values for a property differ (and synchronization needs to occur). As per Shit Gary Says: bit.ly/1j9ou3Q
391 392 393 394 395 396 397 398 399 400 401 402 |
# File 'lib/puppet/provider/mobileconfig.rb', line 391 def flush coalesce_mobileconfig # Collect the resources again once they've been changed (that way `puppet # resource` will show the correct values after changes have been made). all_profiles = self.class.get_installed_profiles this_profile = all_profiles.find do |profile| profile['ProfileIdentifier'].eql? resource[:name] end @property_hash = self.class.get_resource_properties(this_profile) end |
#parse_cert_data_from_blob(blob) ⇒ Object
Validates a Blob as a com.apple.security.pkcs1 Certificate Payload
267 268 269 270 271 272 273 274 275 276 |
# File 'lib/puppet/provider/mobileconfig.rb', line 267 def parse_cert_data_from_blob(blob) begin blob = Base64.decode64(blob) OpenSSL::X509::Certificate.new blob rescue OpenSSL::X509::CertificateError => e raise Puppet::Error, "##{__method__}: Could not parse certificate data! [#{e.}]" end blob end |
#parse_cert_data_from_string(string) ⇒ Object
Validates a String as a com.apple.security.pkcs1 Certificate Payload
-
will decode Base64 if it can
251 252 253 254 255 256 257 258 259 260 261 262 263 264 |
# File 'lib/puppet/provider/mobileconfig.rb', line 251 def parse_cert_data_from_string(string) begin OpenSSL::X509::Certificate.new string rescue OpenSSL::X509::CertificateError begin string = Base64.decode64(string) OpenSSL::X509::Certificate.new string rescue OpenSSL::X509::CertificateError => e raise Puppet::Error, "##{__method__}: Could not parse certificate data! [#{e.}]" end end string end |
#process_certificate_payload(payload) ⇒ Object
Processes the com.apple.security.pkcs1 PayloadContent as required
This provider allows PayloadContent for com.apple.security.pkcs1 to be expressed as PEM (ASCII), Base64 Encoded binary, or a Base64 encoded CFPropertyList::Blob.
Parsable data is validated using OpenSSL.
It should be able to determine what you are passing in, but if it can’t, an Exception is raised.
289 290 291 292 293 294 295 296 297 298 299 300 |
# File 'lib/puppet/provider/mobileconfig.rb', line 289 def process_certificate_payload(payload) data = case payload['PayloadContent'] when CFPropertyList::Blob parse_cert_data_from_blob(payload['PayloadContent']) when String parse_cert_data_from_string(payload['PayloadContent']) else raise Puppet::Error, "Invalid Certificate Data!" end payload['PayloadContent'] = CFPropertyList::Blob.new data payload end |
#transform_content(content) ⇒ Object
Formats and fortifies the PayloadContent array
-
ensures required keys to each Hash
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 |
# File 'lib/puppet/provider/mobileconfig.rb', line 304 def transform_content(content) return [] if content.empty? content.collect! do |payload| # PayloadUUID for each Payload is modified MD5 sum of Payload itself, # minus any of the other ephemeral keys. We can use this to check whether # or not the content has been modified. Even when the Payload attributes # cannot be compared (ie. Password keys). payload.delete('PayloadUUID') = ::ManagedMacCommon::content_to_uuid payload.sort = payload['PayloadIdentifier'] || [@resource[:name], ].join('.') payload.merge!({ 'PayloadIdentifier' => , 'PayloadUUID' => , 'PayloadEnabled' => true, 'PayloadVersion' => 1, }) case payload['PayloadType'] when 'com.apple.DirectoryService.managed' add_activedirectory_keys(payload) when 'com.apple.security.pkcs1' process_certificate_payload(payload) else payload end end end |