Class: Puppet::Provider::MobileConfig

Inherits:
Puppet::Provider
  • Object
show all
Defined in:
lib/puppet/provider/mobileconfig.rb

Class Method Summary collapse

Instance Method Summary collapse

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_profilesObject

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

.instancesObject

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

Raises:

  • (Puppet::Error)


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_mobileconfigObject

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

#createObject



187
188
189
# File 'lib/puppet/provider/mobileconfig.rb', line 187

def create
  @property_flush[:ensure] = :present
end

#destroyObject



191
192
193
# File 'lib/puppet/provider/mobileconfig.rb', line 191

def destroy
  @property_flush[:ensure] = :absent
end

#exists?Boolean

Returns:

  • (Boolean)


195
196
197
# File 'lib/puppet/provider/mobileconfig.rb', line 195

def exists?
  @property_hash[:ensure] == :present
end

#flushObject

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.message}]"
  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.message}]"
    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')
    embedded_payload_uuid = ::ManagedMacCommon::content_to_uuid payload.sort
    embedded_payload_id   = payload['PayloadIdentifier'] || [@resource[:name],
                                    embedded_payload_uuid].join('.')
    payload.merge!({
      'PayloadIdentifier' => embedded_payload_id,
      'PayloadUUID'       => embedded_payload_uuid,
      '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