macadmin 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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