macadmin 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,297 @@
1
+ module MacAdmin
2
+
3
+ # Custom error
4
+ class ShadowHashError < StandardError
5
+ UNSUPPORTED_OBJECT_ERR = 'Unsupported object: cannot store ShadowHashData'
6
+ end
7
+
8
+ # ShadowHash (super class)
9
+ # - common methods for password sub-classes
10
+ class ShadowHash
11
+
12
+ include MacAdmin::Password
13
+
14
+ attr_reader :label
15
+
16
+ class << self
17
+
18
+ # Reads the password data from the user record
19
+ # - returns an appropriate ShadowHash object
20
+ def create_from_user_record(user)
21
+ if user['ShadowHashData']
22
+ password = read_shadowhashdata(user['ShadowHashData'])
23
+ if password[SaltedSHA512::LABEL]
24
+ return SaltedSHA512.create_from_shadowhashdata(password)
25
+ else
26
+ return SaltedSHA512PBKDF2.create_from_shadowhashdata(password)
27
+ end
28
+ else
29
+ if guid = user['generateduid']
30
+ return SaltedSHA1.create_from_shadowhash_file(guid)
31
+ end
32
+ end
33
+ nil
34
+ end
35
+
36
+ # Returns Hash
37
+ # - key: label, value: password data
38
+ def read_shadowhashdata(data)
39
+ plist = CFPropertyList::List.new(:data => data[0].to_s)
40
+ CFPropertyList.native_types(plist.value)
41
+ end
42
+
43
+ end # end self
44
+
45
+ end
46
+
47
+ # Legacy ShadowHashs
48
+ # - password management for Mac OS X 10.6 and below
49
+ # - passwords are managed in separate files in /var/db/shadow/hash
50
+ class SaltedSHA1 < ShadowHash
51
+
52
+ SHADOWHASH_STORE = '/private/var/db/shadow/hash'
53
+
54
+ attr_accessor :hash
55
+
56
+ class << self
57
+
58
+ def create_from_shadowhash_file(guid)
59
+ file = "#{SHADOWHASH_STORE}/#{guid}"
60
+ if File.exists? file
61
+ hash = read_from_shadowhash_file(file)
62
+ return nil unless hash
63
+ self.new(hash)
64
+ end
65
+ end
66
+
67
+ def read_from_shadowhash_file(file)
68
+ content = File.readlines(file).first
69
+ content[168,48]
70
+ end
71
+
72
+ end
73
+
74
+ # Initializes a SaltedSHA512 ShadowHash object from string
75
+ # - string param should be a hex string, 24 bytes
76
+ def initialize(string)
77
+ @hash = validate(string)
78
+ end
79
+
80
+ # Validates the string param
81
+ # - ensure the string param is hex string 24 bytes long
82
+ def validate(string)
83
+ error = "Invalid: arg must be hexadecimal string (24 bytes)"
84
+ raise ArgumentError.new(error) unless string =~ /([A-F0-9]{2}){24}/
85
+ string
86
+ end
87
+
88
+ # Return a String representation of the ShadowHash data
89
+ def password
90
+ @hash.to_s
91
+ end
92
+
93
+ # Return the ShadowHash as a Salted SHA1 String
94
+ def data
95
+ @data ||= @hash.to_s
96
+ end
97
+
98
+ private
99
+
100
+ # Pseudo callback
101
+ # - this method does not modify User objects
102
+ # - SHA1 passwords are not stored as part of the User object
103
+ # - They're stored in separate files on disk
104
+ def store(sender)
105
+ raise ShadowHashError.new(ShadowHashError::UNSUPPORTED_OBJECT_ERR) unless sender.is_a? MacAdmin::User
106
+ @data = @hash.to_s
107
+ end
108
+
109
+ # Write password to file
110
+ # - success based on the length of the file, must be 1240 bytes
111
+ # - returns boolean
112
+ def write_to_shadowhash_file(sender)
113
+ raise ShadowHashError.new(ShadowHashError::UNSUPPORTED_OBJECT_ERR) unless sender.is_a? MacAdmin::User
114
+ path = "#{SHADOWHASH_STORE}/#{sender[:generateduid].first}"
115
+ file = File.open(path, mode='w+')
116
+ content = file.read
117
+ content = "0" * 1240 if content.length < 1240
118
+ file.rewind
119
+ content[168...(168 + 48)] = @hash.to_s
120
+ file.write content
121
+ file.close
122
+ File.size(path) == 1240
123
+ end
124
+ alias :to_file :write_to_shadowhash_file
125
+
126
+ # Remove the ShadowHash file associated with the sender
127
+ # - returns boolean
128
+ def remove_shadowhash_file(sender)
129
+ raise ShadowHashError.new(ShadowHashError::UNSUPPORTED_OBJECT_ERR) unless sender.is_a? MacAdmin::User
130
+ path = "#{SHADOWHASH_STORE}/#{sender[:generateduid].first}"
131
+ FileUtils.rm path if File.exists? path
132
+ !File.exists? path
133
+ end
134
+ alias :rm_file :remove_shadowhash_file
135
+
136
+ end
137
+
138
+ # Lion ShadowHashs
139
+ # - Mac OS X 10.7 store passwords as Salted SHA512 hashes
140
+ # - hash is stored directly in the user's plist
141
+ class SaltedSHA512 < ShadowHash
142
+
143
+ LABEL = 'SALTED-SHA512'
144
+
145
+ attr_accessor :hash
146
+
147
+ class << self
148
+
149
+ # Constructs a SaltedSHA512 ShadowHash object from ShadowHashData
150
+ # - param is raw ShadowHashData object
151
+ def create_from_shadowhashdata(data)
152
+ value = data[SaltedSHA512::LABEL].to_s
153
+ hex = MacAdmin::Password.convert_to_hex(value)
154
+ self.new(hex)
155
+ end
156
+
157
+ end
158
+
159
+ # Initializes a SaltedSHA512 ShadowHash object from string
160
+ # - string param should be a hex string, 68 bytes
161
+ def initialize(string)
162
+ @label = LABEL
163
+ @hash = validate(string)
164
+ end
165
+
166
+ # Validates the string param
167
+ # - ensure the string param is hex string 68 bytes long
168
+ def validate(string)
169
+ error = "Invalid: arg must be hexadecimal string (68 bytes)"
170
+ raise ArgumentError.new(error) unless string =~ /([a-f0-9]{2}){68}/
171
+ string
172
+ end
173
+
174
+ # Return the ShadowHash as a ShadowHashData object
175
+ # - Binary Plist
176
+ def data
177
+ @data ||= { @label => convert_to_blob(@hash) }.to_plist
178
+ end
179
+
180
+ # Return a Hash representation of the ShadowHash data
181
+ def password
182
+ { @label => @hash }
183
+ end
184
+
185
+ private
186
+
187
+ # Pseudo callback for inserting a ShadowHashData object into the User object
188
+ def store(sender)
189
+ raise ShadowHashError.new(ShadowHashError::UNSUPPORTED_OBJECT_ERR) unless sender.is_a? MacAdmin::User
190
+ @data = { @label => convert_to_blob(@hash) }.to_plist
191
+ sender['ShadowHashData'] = [@data]
192
+ end
193
+
194
+ end
195
+
196
+ # Current ShadowHash Scheme
197
+ # - Mac OS X 10.8 and up store passwords as Salted SHA512-PBKDF2 hashes
198
+ # - hash is stored directly in the user's plist
199
+ class SaltedSHA512PBKDF2 < ShadowHash
200
+
201
+ LABEL = 'SALTED-SHA512-PBKDF2'
202
+
203
+ class << self
204
+
205
+ # Constructs a SaltedSHA512PBKDF2 ShadowHash object from ShadowHashData
206
+ # - param is raw ShadowHashData object
207
+ def create_from_shadowhashdata(data)
208
+ hash = data[SaltedSHA512PBKDF2::LABEL]
209
+ hash = hash.inject({}) do |memo, (key, value)|
210
+ if key.eql? 'iterations'
211
+ value = value.to_i
212
+ else
213
+ value = MacAdmin::Password.convert_to_hex(value)
214
+ end
215
+ memo[key.to_sym] = value
216
+ memo
217
+ end
218
+ self.new(hash)
219
+ end
220
+
221
+ end
222
+
223
+ # Initializes a SaltedSHA512PBKDF2 ShadowHash object from Hash or Array
224
+ # - if passing an array, you must order the elements: entropy, salt, iterations
225
+ # - pass a Hash with keys: entropy, salt, iterations
226
+ def initialize(args)
227
+ @label = LABEL
228
+ @hash = validate(args)
229
+ end
230
+
231
+ # Validates the params
232
+ # - ensure that we have the required params
233
+ # - an Array that maps to required_keys structure
234
+ # - a Hash that contains the required keys
235
+ # - all values are qualified according to requirements
236
+ def validate(args)
237
+ error = nil
238
+ hash = nil
239
+ required_keys = [:entropy, :salt, :iterations]
240
+ if args.is_a? Array
241
+ hash = Hash[required_keys.zip(args)]
242
+ elsif args.is_a? Hash
243
+ hash = args
244
+ end
245
+ # validate hash
246
+ unless (hash.keys - required_keys).empty?
247
+ error = "Invalid: args must contain, #{required_keys.join(', ')}"
248
+ end
249
+ unless hash[:entropy] =~ /([a-f0-9]{2}){128}/
250
+ error = "Invalid: entropy must be hexadecimal string (128 bytes)"
251
+ end
252
+ unless hash[:salt] =~ /([a-f0-9]{2}){32}/
253
+ error = "Invalid: salt must be hexadecimal string (32 bytes)"
254
+ end
255
+ unless hash[:iterations] >= 0
256
+ error = "Invalid: entropy must positive integer"
257
+ end
258
+ raise ArgumentError.new(error) if error
259
+ hash
260
+ end
261
+
262
+ # Return the ShadowHash as a ShadowHashData object
263
+ # - Binary Plist
264
+ def data
265
+ @data ||= { @label => format(@hash) }.to_plist
266
+ end
267
+
268
+ # Return a Hash representation of the ShadowHash data
269
+ def password
270
+ { @label => @hash }
271
+ end
272
+
273
+ private
274
+
275
+ # Pseudo callback for inserting a ShadowHashData object into the User object
276
+ def store(sender)
277
+ raise ShadowHashError.new(ShadowHashError::UNSUPPORTED_OBJECT_ERR) unless sender.is_a? MacAdmin::User
278
+ @data = { @label => format(@hash) }.to_plist
279
+ sender['ShadowHashData'] = [@data]
280
+ end
281
+
282
+ # Format the password data for ShadowHashData object compatibility
283
+ def format(hash)
284
+ hash.inject({}) do |memo, (key, value)|
285
+ if key.to_s.eql? 'iterations'
286
+ value = value.to_i
287
+ else
288
+ value = convert_to_blob value
289
+ end
290
+ memo[key.to_s] = value
291
+ memo
292
+ end
293
+ end
294
+
295
+ end
296
+
297
+ end
@@ -0,0 +1,8 @@
1
+ module MacAdmin
2
+ VERSION = '0.0.4'
3
+
4
+ def self.version_string
5
+ "macadmin version, #{MacAdmin::VERSION}"
6
+ end
7
+
8
+ end
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'macadmin/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "macadmin"
8
+ s.version = MacAdmin::VERSION
9
+ s.date = '2012-07-08'
10
+ s.authors = ["Brian Warsing"]
11
+ s.email = ['dayglojesus@gmail.com']
12
+ s.description = "Gem to assist in performing common systems administration tasks in Mac OS X"
13
+ s.summary = "Ruby Mac Systems Administration Library"
14
+ s.homepage = "http://github.com/dayglojesus/macadmin"
15
+ s.license = "MIT"
16
+ s.files = `git ls-files`.split($/)
17
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
19
+ s.require_paths = ["lib", "ext"]
20
+ s.extensions = Dir['ext/**/extconf.rb']
21
+
22
+ s.platform = Gem::Platform::RUBY
23
+
24
+ s.add_development_dependency "bundler", "~> 1.3"
25
+ s.add_development_dependency "rake"
26
+ s.add_development_dependency "rake-compiler"
27
+ s.add_development_dependency "rspec"
28
+ s.add_development_dependency "CFPropertyList"
29
+
30
+ s.add_runtime_dependency "bundler", "~> 1.3"
31
+ s.add_runtime_dependency "rake"
32
+ s.add_runtime_dependency "rake-compiler"
33
+ s.add_runtime_dependency "CFPropertyList"
34
+
35
+ end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ describe MacAdmin::Common do
4
+
5
+ describe 'MAC_OS_X_PRODUCT_VERSION' do
6
+ it 'is a float greater than 10' do
7
+ version = MacAdmin::Common::MAC_OS_X_PRODUCT_VERSION
8
+ version.should > 10
9
+ end
10
+ end
11
+
12
+ describe '#load_plist' do
13
+ it 'should return an Hash object' do
14
+ file = '/System/Library/CoreServices/SystemVersion.plist'
15
+ MacAdmin::Common::load_plist(file).should be_an_instance_of Hash
16
+ end
17
+ end
18
+
19
+ end
20
+
21
+ describe MacAdmin::Common::UUID do
22
+
23
+ before :all do
24
+ @uuid = '897A6343-628F-4964-80F1-C86D0FFA3F91'
25
+ end
26
+
27
+ describe '#new' do
28
+ it 'should return a UUID string' do
29
+ UUID.new.should =~ /([A-Z0-9]{8})-([A-Z0-9]{4}-){3}([A-Z0-9]{12})/
30
+ end
31
+ end
32
+
33
+ describe '#match' do
34
+ it 'should match and return a UUID in _any_ string' do
35
+ UUID.match("com.apple.loginwindow.#{@uuid}.plist").should == @uuid
36
+ end
37
+ end
38
+
39
+ describe '#valid?' do
40
+ it 'should return true if handed a valid UUID string' do
41
+ UUID.valid?(@uuid).should == true
42
+ end
43
+
44
+ it 'should return false if handed a _bad_ UUID string' do
45
+ UUID.valid?(@uuid.chop).should == false
46
+ end
47
+ end
48
+
49
+ end
@@ -0,0 +1,133 @@
1
+ require 'spec_helper'
2
+ require 'fileutils'
3
+
4
+ describe MacAdmin::Computer do
5
+
6
+ before :all do
7
+ # Create a dslocal sandbox
8
+ @test_dir = "/private/tmp/macadmin_computer_test.#{rand(100000)}"
9
+ MacAdmin::DSLocalRecord.send(:remove_const, :DSLOCAL_ROOT)
10
+ MacAdmin::DSLocalRecord::DSLOCAL_ROOT = File.expand_path @test_dir
11
+ FileUtils.mkdir_p "#{@test_dir}/Default/computers"
12
+
13
+ # Create two computer record plists that we can load
14
+ planet_express = {
15
+ "en_address" => ["aa:aa:aa:aa:aa:aa"],
16
+ "realname" => ["Planet Express"],
17
+ "name" => ["planet-express"],
18
+ "generateduid" => ["00000000-0000-0000-0000-000000000001"],
19
+ }
20
+
21
+ [planet_express].each do |computer|
22
+ record = CFPropertyList::List.new
23
+ record.value = CFPropertyList.guess(computer)
24
+ record.save("#{@test_dir}/Default/computers/#{computer['name'].first}.plist", CFPropertyList::List::FORMAT_BINARY)
25
+ end
26
+
27
+ end
28
+
29
+ describe '#new' do
30
+
31
+ context 'throws an ArgumentError when given fewer than 1 params' do
32
+ subject { Computer.new }
33
+ it { expect { subject }.to raise_error(ArgumentError) }
34
+ end
35
+
36
+ context 'when created from a parameter Hash' do
37
+ subject { Computer.new :name => 'nimbus', :en_address => 'dd:dd:dd:dd:dd:dd' }
38
+ it { should be_an_instance_of Computer }
39
+ it 'should have a valid generateduid attribute' do
40
+ (UUID.valid?(subject.generateduid.first)).should be_true
41
+ end
42
+ its (:name) { should eq ['nimbus'] }
43
+ its (:realname) { should eq ['nimbus'.capitalize] }
44
+ its (:en_address) { should eq ['dd:dd:dd:dd:dd:dd'] }
45
+ end
46
+
47
+ context 'when created from a just a name (String)' do
48
+ subject { Computer.new 'nimbus' }
49
+ it { should be_an_instance_of Computer }
50
+ it 'should have a valid generateduid attribute' do
51
+ (UUID.valid?(subject.generateduid.first)).should be_true
52
+ end
53
+ its (:name) { should eq ['nimbus'] }
54
+ its (:realname) { should eq ['nimbus'.capitalize] }
55
+ its (:en_address) { should eq [MacAdmin::Common.get_primary_mac_address] }
56
+ end
57
+
58
+ end
59
+
60
+ describe '#exists?' do
61
+
62
+ context "when the object is different from its associated file" do
63
+ subject { Computer.new :name => 'planet-express', :en_address => "ff:ff:ff:ff:ff:ff" }
64
+ it 'has an associated file' do
65
+ File.exists?(subject.file).should be_true
66
+ end
67
+ it 'returns false because file and object do not match' do
68
+ subject.exists?.should be_false
69
+ end
70
+ end
71
+
72
+ context "when ALL the object's attributes match its associated file" do
73
+ subject { Computer.new :name => 'planet-express', :en_address => "aa:aa:aa:aa:aa:aa" }
74
+ it 'has an associated file' do
75
+ File.exists?(subject.file).should be_true
76
+ end
77
+ it 'returns false because file and object do not match' do
78
+ subject.exists?.should be_true
79
+ end
80
+ end
81
+
82
+ context "when the object does not have an associated file on disk" do
83
+ subject { Computer.new :name => 'nimbus' }
84
+ it 'does not have an associated file' do
85
+ File.exists?(subject.file).should be_false
86
+ end
87
+ it 'should return false' do
88
+ subject.exists?.should be_false
89
+ end
90
+ end
91
+
92
+ end
93
+
94
+ describe '#create' do
95
+
96
+ context "with NO path parameter (default)" do
97
+ subject { Computer.new :name => 'nimbus', :en_address => "bb:bb:bb:bb:bb:bb" }
98
+ it 'saves the record to disk on the derived path' do
99
+ subject.create.should be_true
100
+ File.exists?(subject.file).should be_true
101
+ end
102
+ after do
103
+ FileUtils.rm_rf subject.file
104
+ end
105
+ end
106
+
107
+ context "with a path parameter" do
108
+ path = "/private/tmp/group-create-method-test.plist"
109
+ subject { Computer.new :name => 'nimbus', :en_address => "bb:bb:bb:bb:bb:bb" }
110
+ it 'saves the record to disk on the path specified' do
111
+ subject.create(path).should be_true
112
+ end
113
+ after do
114
+ FileUtils.rm_rf path
115
+ end
116
+ end
117
+
118
+ end
119
+
120
+ describe '#destroy' do
121
+ subject { Computer.new :name => 'nimbus', :en_address => "bb:bb:bb:bb:bb:bb" }
122
+ it 'removes the record on disk and returns true' do
123
+ subject.create
124
+ subject.destroy.should be_true
125
+ File.exists?(subject.file).should be_false
126
+ end
127
+ end
128
+
129
+ after :all do
130
+ FileUtils.rm_rf @test_dir
131
+ end
132
+
133
+ end