macadmin 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,82 @@
1
+ module MacAdmin
2
+
3
+ # Group
4
+ # - creates and manages Mac OS X User Groups
5
+ # - params: :name, :realname, :gid
6
+ class Group < DSLocalRecord
7
+
8
+ MIN_GID = 501
9
+
10
+ def initialize(args)
11
+ @member_class = User unless defined? @member_class
12
+ @group_class = Group unless defined? @group_class
13
+ super args
14
+ end
15
+
16
+ # Examine the object's users array for "member"
17
+ # - single param: the name of a user (String)
18
+ def has_user?(member)
19
+ self[:users].member? member
20
+ end
21
+
22
+ # Add a member to the object's users array
23
+ # - single param: the name of a user (String)
24
+ def add_user(member)
25
+ user = @member_class.new :name => member, :node => self.node
26
+ raise unless user.exists?
27
+ self[:users] << member
28
+ self[:users].uniq!
29
+ end
30
+
31
+ # Remove a member from the object's users array
32
+ # - single param: the name of a user (String)
33
+ def rm_user(member)
34
+ self[:users].delete member
35
+ end
36
+
37
+ # Examine the object's groupmembers array for "member"
38
+ # - single param: the name of a group (String)
39
+ def has_groupmember?(member)
40
+ group = @group_class.new :name => member, :node => self.node
41
+ return false unless group.exists?
42
+ self[:groupmembers].member? group.generateduid.first
43
+ end
44
+
45
+ # Add a member to the object's groupmembers array
46
+ # - single param: the name of a group (String)
47
+ def add_groupmember(member)
48
+ group = @group_class.new :name => member, :node => self.node
49
+ raise unless group.exists?
50
+ self[:groupmembers] << group.generateduid.first
51
+ self[:groupmembers].uniq!
52
+ end
53
+
54
+ # Remove a member from the object's groupmembers array
55
+ # - single param: the name of a group (String)
56
+ def rm_groupmember(member)
57
+ group = @group_class.new :name => member, :node => self.node
58
+ return nil unless group.exists?
59
+ self[:groupmembers].delete group.generateduid.first
60
+ end
61
+
62
+ private
63
+
64
+ # Handle required but unspecified record attributes
65
+ # - generates missing attributes
66
+ # - changes are merged into the composite record
67
+ def defaults(data)
68
+ records = all_records(@node)
69
+ next_gid = next_id(MIN_GID, get_all_attribs_of_type(:gid, records))
70
+ defaults = {
71
+ 'realname' => ["#{data['name'].first.capitalize}"],
72
+ 'gid' => ["#{next_gid}"],
73
+ 'passwd' => ['*'],
74
+ 'groupmembers' => [],
75
+ 'users' => [],
76
+ }
77
+ super defaults.merge(data)
78
+ end
79
+
80
+ end
81
+
82
+ end
@@ -0,0 +1,113 @@
1
+ module MacAdmin
2
+
3
+ # User
4
+ # - creates and manages Mac OS X User accounts
5
+ # - params: :name, :realname, :password, :uid, :gid, :shell, :home, :comment
6
+ class User < DSLocalRecord
7
+
8
+ MIN_UID = 501
9
+
10
+ # Override parent initialization
11
+ # - capture the password if it exists
12
+ # - if there's no password object param, try to get one
13
+ # - shoehorn the password into the User record
14
+ def initialize(args)
15
+ if args.respond_to?(:keys)
16
+ @password = args.delete(:password)
17
+ end
18
+ super(args)
19
+ if @password
20
+ self.send(:password=)
21
+ else
22
+ @password = ShadowHash.create_from_user_record(self)
23
+ end
24
+ end
25
+
26
+ # Generic setter
27
+ # - Accepts a ShadowHash object
28
+ # - delegates the storage operation to the ShadowHash object itself
29
+ def password=(password = @password)
30
+ error = 'Argument was not a ShadowHash object'
31
+ unless password.nil? or password.respond_to? :password
32
+ raise ArgumentError.new(error)
33
+ end
34
+ @password = password
35
+ @password.send(:store, self) unless @password.nil?
36
+ end
37
+
38
+ # Generic getter
39
+ # - Returns Ruby Hash representation of the User's password
40
+ def password
41
+ return nil unless @password
42
+ @password.password
43
+ end
44
+
45
+ # Legacy user records are determined by SHA1 password type
46
+ # - returns boolean
47
+ def legacy?
48
+ @password.is_a? SaltedSHA1
49
+ end
50
+
51
+ # Does the specified resource already exist?
52
+ # - overrides parent method; required for Legacy users
53
+ # - checks the password if SHA1 and kicks up to parent
54
+ def exists?
55
+ if self.legacy?
56
+ password_on_disk = SaltedSHA1.create_from_shadowhash_file self.generateduid[0]
57
+ return false unless password_on_disk
58
+ return false unless password_on_disk.password.eql? @password.password
59
+ end
60
+ super
61
+ end
62
+
63
+ # Create the record
64
+ # - overrides parent method; required for Legacy users
65
+ # - creates the password if SHA1 and kicks up to parent
66
+ def create(file=@file)
67
+ if self.legacy?
68
+ return unless @password.send(:to_file, self)
69
+ end
70
+ super
71
+ end
72
+
73
+ # Delete the record
74
+ # - overrides parent method; required for Legacy users
75
+ # - destroys the password if SHA1 and kicks up to parent
76
+ def destroy(file=@file)
77
+ if self.legacy?
78
+ return unless @password.send(:rm_file, self)
79
+ end
80
+ super
81
+ end
82
+
83
+ # Return a Puppet style resource manifest
84
+ # - need to find a sensible way of doing this
85
+ # - need to move this method into the Parent class
86
+ def to_puppet
87
+ puts "not implemented"
88
+ end
89
+
90
+ private
91
+
92
+ # Handle required but unspecified record attributes
93
+ # - generates missing attributes
94
+ # - changes are merged into the composite record
95
+ def defaults(data)
96
+ records = all_records(@node)
97
+ next_uid = next_id(MIN_UID, get_all_attribs_of_type(:uid, records))
98
+ defaults = {
99
+ 'realname' => ["#{data['name'].first}"],
100
+ 'uid' => ["#{next_uid}"],
101
+ 'home' => ["/Users/#{data['name'].first}"],
102
+ 'shell' => ['/bin/bash'],
103
+ 'gid' => ['20'],
104
+ 'passwd' => ['********'],
105
+ 'comment' => [''],
106
+ }
107
+ super defaults.merge(data)
108
+ end
109
+
110
+ end
111
+
112
+ end
113
+
@@ -0,0 +1,227 @@
1
+ module MacAdmin
2
+
3
+ # MCX
4
+ # - methods and classes mixed into MacAdmin::DSLocalRecord for managing MCX policy
5
+ module MCX
6
+
7
+ # Policy
8
+ # - document format for mcx_export
9
+ class Policy
10
+
11
+ MANIFESTS = '/System/Library/CoreServices/ManagedClient.app/Contents/Resources'
12
+
13
+ def initialize(mcx_settings)
14
+ @documents = mcx_settings
15
+ @policy = process_documents(@documents)
16
+ end
17
+
18
+ # Dump the document in a human-readable format
19
+ def to_plist
20
+ @policy.to_plist({:plist_format => CFPropertyList::List::FORMAT_XML, :formatted => true})
21
+ end
22
+
23
+ private
24
+
25
+ # Process each of the domain documents
26
+ def process_documents(documents)
27
+ documents.inject({}) do |dict, doc|
28
+ plist = CFPropertyList::List.new(:data => doc)
29
+ native = CFPropertyList.native_types(plist.value)
30
+ native['mcx_application_data'].inject({}) do |result, (domain, domain_dict)|
31
+ dict[domain] = process_domain(domain, domain_dict)
32
+ dict
33
+ end
34
+ end
35
+ end
36
+
37
+ # Process the domain preference document
38
+ # - flattens and reformats the domain document into constituent keys
39
+ def process_domain(domain, domain_dict)
40
+ manifest = "#{MANIFESTS}/#{domain}.manifest/Contents/Resources/#{domain}.manifest"
41
+ upk_subkeys = load_upk_subkeys(manifest)
42
+ domain_dict.inject({}) do |result, (enforcement, enforcement_array)|
43
+ enforcement_array.inject({}) do |hash, dict|
44
+ state = dict.include?('mcx_data_timestamp') ? 'once' : 'often'
45
+ state = 'always' if enforcement.eql? 'Forced'
46
+ dict['mcx_preference_settings'].each do |name, value|
47
+ result[name] = { 'state' => state, 'value' => value }
48
+ if upk_subkeys
49
+ upk = get_upk_info(name, state, upk_subkeys)
50
+ result[name]['upk'] = get_upk_info(name, state, upk_subkeys) if upk
51
+ end
52
+ result
53
+ end
54
+ end
55
+ result
56
+ end
57
+ end
58
+
59
+ # Load the domain's preference manifest and return the UPK subkeys
60
+ # - returns Array
61
+ def load_upk_subkeys(manifest)
62
+ upk_subkeys = []
63
+ if File.exists? manifest
64
+ plist = CFPropertyList::List.new(:file => manifest)
65
+ native = CFPropertyList.native_types(plist.value)
66
+ upk_subkeys = native['pfm_subkeys'].select do |dict|
67
+ if dict['pfm_type'].eql? 'union policy'
68
+ dict
69
+ end
70
+ end
71
+ end
72
+ upk_subkeys
73
+ end
74
+
75
+ # Determine the most appropriate UPK keys for the given preference
76
+ # - returns Hash
77
+ def get_upk_info(name, state, upk_subkeys)
78
+ info = nil
79
+ if upk_subkeys
80
+ results = upk_subkeys.inject({}) do |results, dict|
81
+ if dict['pfm_upk_input_keys'].include? name
82
+ results[dict['pfm_upk_output_name']] = dict
83
+ end
84
+ results
85
+ end
86
+ if results
87
+ if results.size > 1
88
+ if state.eql? 'always'
89
+ results = results.select { |k,v| v if k =~ /-managed\z/ }
90
+ info = results.flatten.last
91
+ else
92
+ results = results.select { |k,v| v if k =~ /\Auser\z/ }
93
+ info = results.flatten.last
94
+ end
95
+ else
96
+ info = results.first
97
+ end
98
+ end
99
+ end
100
+ if info
101
+ upk = { 'mcx_input_key_names' => info['pfm_upk_input_keys'],
102
+ 'mcx_output_key_name' => info['pfm_upk_output_name'],
103
+ 'mcx_remove_duplicates' => info['pfm_remove_duplicates']
104
+ }
105
+ return upk
106
+ end
107
+ nil
108
+ end
109
+
110
+ end
111
+
112
+ # EmbeddedDocument
113
+ # - domain level MCX document suitable for storage in the record's mcx_settings array
114
+ class EmbeddedDocument
115
+
116
+ attr_reader :document
117
+
118
+ def initialize(domain, content)
119
+ @document = { 'mcx_application_data' => {} }
120
+ @domain = domain
121
+ @content = process(content)
122
+ end
123
+
124
+ def formatted
125
+ @document.to_plist({:plist_format => CFPropertyList::List::FORMAT_XML, :formatted => true})
126
+ end
127
+
128
+ def escaped
129
+ CGI.escapeHTML @document.to_plist({:plist_format => CFPropertyList::List::FORMAT_XML, :formatted => true})
130
+ end
131
+
132
+ private
133
+
134
+ def process(content)
135
+ @document['mcx_application_data'][@domain] = {}
136
+ content.each do |pref_name, pref_dict|
137
+ state = pref_dict['state']
138
+ enforcement = (state.eql?('always') ? 'Forced' : 'Set-Once')
139
+ @document['mcx_application_data'][@domain][enforcement] ||= [{}]
140
+ if pref_dict['upk']
141
+ @document['mcx_application_data'][@domain][enforcement][0]['mcx_union_policy_keys'] ||= []
142
+ @document['mcx_application_data'][@domain][enforcement][0]['mcx_union_policy_keys'] << pref_dict['upk']
143
+ end
144
+ @document['mcx_application_data'][@domain][enforcement][0]['mcx_preference_settings'] ||= {}
145
+ @document['mcx_application_data'][@domain][enforcement][0]['mcx_preference_settings'][pref_name] = pref_dict['value']
146
+ if state.eql? 'once'
147
+ @document['mcx_application_data'][@domain][enforcement][0]['mcx_data_timestamp'] = CFPropertyList::CFDate.parse_date(Time.now.utc.xmlschema)
148
+ end
149
+ end
150
+ end
151
+
152
+ end
153
+
154
+ # Settings
155
+ # - class representing the structure of the mcx_settings array
156
+ class Settings
157
+
158
+ attr_reader :domains
159
+
160
+ XML_TAG = '^<\?xml\sversion="1\.0"\sencoding="UTF-8"\?>'
161
+
162
+ class << self
163
+ def init_with_file(path)
164
+ content = load_plist(path)
165
+ self.new(content)
166
+ end
167
+ end
168
+
169
+ def initialize(content, type=:data)
170
+ type = :file unless content =~ /#{XML_TAG}/
171
+ plist = CFPropertyList::List.new(type => content)
172
+ native = CFPropertyList.native_types(plist.value)
173
+ @domains = []
174
+ @content = process(native)
175
+ end
176
+
177
+ private
178
+
179
+ def process(content)
180
+ content.each do |domain_name, domain_dict|
181
+ doc = EmbeddedDocument.new domain_name, domain_dict
182
+ @domains << doc.formatted
183
+ end
184
+ end
185
+
186
+ end
187
+
188
+ # Import MCX Content and apply it to the current object
189
+ # - accepts a single parameter: path to Plist file containing exported MCX policy or a string of XML content representing the MCX policy
190
+ # - re-formats the imported MCX for storage on the record and adds the two require attributes: mcx_flags and mcx_settings
191
+ # - current implmentation replaces policy wholesale (no append)
192
+ def mcx_import(content, append=false)
193
+ settings = Settings.new content
194
+ mcx_flags = { 'has_mcx_settings' => true }
195
+ mcx_flags = mcx_flags.to_plist({:plist_format => CFPropertyList::List::FORMAT_XML, :formatted => true})
196
+ self['mcx_flags'] = [CFPropertyList::Blob.new(mcx_flags)]
197
+ self['mcx_settings'] = settings.domains
198
+ end
199
+ alias :mcximport :mcx_import
200
+
201
+ # Export the MCX preferences for the record
202
+ def mcx_export
203
+ doc = Policy.new self['mcx_settings']
204
+ doc.to_plist
205
+ end
206
+ alias :mcxexport :mcx_export
207
+
208
+ # Pretty print the contents of the record's mcx_settings array
209
+ def pretty_mcx
210
+ self['mcx_settings'].collect { |doc| CGI.unescapeHTML doc }
211
+ end
212
+
213
+ # Does the object have any MCX policy?
214
+ def has_mcx?
215
+ self.has_key? 'mcx_settings' and self['mcx_settings'].is_a? Array and not self['mcx_settings'].empty?
216
+ end
217
+
218
+ # Remove all MCX policy from the object
219
+ def mcx_delete
220
+ self.delete('mcx_flags')
221
+ self.delete('mcx_settings')
222
+ end
223
+ alias :mcxdelete :mcx_delete
224
+
225
+ end
226
+
227
+ end
@@ -0,0 +1,72 @@
1
+ module MacAdmin
2
+
3
+ # Password
4
+ # - module containing methods for converting a plain String into Mac OS X password hash
5
+ module Password
6
+
7
+ require 'openssl'
8
+ require 'securerandom'
9
+
10
+ extend self
11
+
12
+ # Convert ASCII string to hex bytes
13
+ def convert_to_hex(string)
14
+ string.unpack('H*').first
15
+ end
16
+
17
+ # Convert hex string to CFBlob
18
+ def convert_to_blob(hex)
19
+ ascii = hex.scan(/../).collect { |byte| byte.hex.chr }.join
20
+ CFPropertyList::Blob.new(ascii)
21
+ end
22
+
23
+ # This method is only available in Mountain Lion or better
24
+ if MacAdmin::Common::MAC_OS_X_PRODUCT_VERSION > 10.7
25
+
26
+ require "macadmin/password/crypto"
27
+
28
+ # Creates a SaltedSHA512PBKDF2 password from String
29
+ # - single param: String
30
+ # - returns: SaltedSHA512PBKDF2
31
+ def salted_sha512_pbkdf2(password)
32
+ hash = salted_sha512_pbkdf2_from_string password
33
+ SaltedSHA512PBKDF2.new hash
34
+ end
35
+
36
+ end
37
+
38
+ # Creates a SaltedSHA512 password from String
39
+ # - single param: String
40
+ # - returns: SaltedSHA512
41
+ def salted_sha512(password)
42
+ salt = SecureRandom.random_bytes(4)
43
+ hash = Digest::SHA512.hexdigest(salt + password)
44
+ SaltedSHA512.new(convert_to_hex(salt) + hash)
45
+ end
46
+
47
+ # Creates a SaltedSHA1 password from String
48
+ # - single param: String
49
+ # - returns: SaltedSHA1
50
+ def salted_sha1(password)
51
+ salt = SecureRandom.random_bytes(4)
52
+ hash = Digest::SHA1.hexdigest(salt + password)
53
+ SaltedSHA1.new((convert_to_hex(salt) + hash).upcase)
54
+ end
55
+
56
+ # Create a platform appropriate password
57
+ # - single param: String
58
+ # - returns: SaltedSHA512PBKDF2 or SaltedSHA512 or SaltedSHA1 depending on platform
59
+ def apropos(password)
60
+ platform = MacAdmin::Common::MAC_OS_X_PRODUCT_VERSION
61
+ if platform >= 10.8
62
+ return salted_sha512_pbkdf2 password
63
+ elsif platform == 10.7
64
+ return salted_sha512 password
65
+ else
66
+ return salted_sha1 password
67
+ end
68
+ end
69
+
70
+ end
71
+
72
+ end