macadmin 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,343 @@
1
+ require 'spec_helper'
2
+
3
+ describe MacAdmin::DSLocalNode do
4
+
5
+ before :all do
6
+ require 'etc'
7
+ this_user = Etc.getpwnam(Etc.getlogin)
8
+ this_uid = this_user.uid
9
+ this_gid = this_user.gid
10
+ # Create a dslocal sandbox
11
+ @test_dir = "/private/tmp/macadmin_dslocalnode_test.#{rand(100000)}"
12
+ @child_dirs = ['aliases', 'computer_lists', 'computergroups', 'computers', 'config', 'groups', 'networks', 'users']
13
+ MacAdmin::DSLocalNode.send(:remove_const, :SANDBOX_FILE)
14
+ MacAdmin::DSLocalNode.send(:remove_const, :DSLOCAL_ROOT)
15
+ MacAdmin::DSLocalNode.send(:remove_const, :PREFERENCES)
16
+ MacAdmin::DSLocalNode.send(:remove_const, :PREFERENCES_LEGACY)
17
+ MacAdmin::DSLocalNode.send(:remove_const, :OWNER)
18
+ MacAdmin::DSLocalNode.send(:remove_const, :GROUP)
19
+ MacAdmin::DSLocalNode::DSLOCAL_ROOT = File.expand_path @test_dir
20
+ MacAdmin::DSLocalNode::SANDBOX_FILE = File.expand_path "#{@test_dir}/opendirectoryd.sb"
21
+ MacAdmin::DSLocalNode::PREFERENCES = File.expand_path "#{@test_dir}/config.plist"
22
+ MacAdmin::DSLocalNode::PREFERENCES_LEGACY = File.expand_path "#{@test_dir}/legacy.plist"
23
+ MacAdmin::DSLocalNode::OWNER = this_uid
24
+ MacAdmin::DSLocalNode::GROUP = this_gid
25
+ @root = "#{@test_dir}/Default"
26
+ FileUtils.mkdir_p @root
27
+ begin
28
+ FileUtils.mkdir_p @root unless File.exist? @root
29
+ FileUtils.chmod(0700, @root)
30
+ @child_dirs.each do |child|
31
+ FileUtils.mkdir_p("#{@root}/#{child}") unless File.exist?("#{@root}/#{child}")
32
+ FileUtils.chmod(0700, "#{@root}/#{child}")
33
+ end
34
+ FileUtils.chown_R this_uid, this_gid, @root
35
+ rescue Exception => e
36
+ p e.message
37
+ end
38
+
39
+ @config = <<-CONFIG
40
+ <?xml version="1.0" encoding="UTF-8"?>
41
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
42
+ <plist version="1.0">
43
+ <dict>
44
+ <key>comment</key>
45
+ <string>Default search policy</string>
46
+ <key>enabled</key>
47
+ <true/>
48
+ <key>mappings</key>
49
+ <dict/>
50
+ <key>modules</key>
51
+ <dict>
52
+ <key>session</key>
53
+ <array>
54
+ <dict>
55
+ <key>module</key>
56
+ <string>search</string>
57
+ <key>options</key>
58
+ <dict>
59
+ <key>dsAttrTypeStandard:CSPSearchPath</key>
60
+ <array>
61
+ <string>/Local/Default</string>
62
+ </array>
63
+ <key>dsAttrTypeStandard:LSPSearchPath</key>
64
+ <array>
65
+ <string>/Local/Default</string>
66
+ </array>
67
+ <key>dsAttrTypeStandard:NSPSearchPath</key>
68
+ <array>
69
+ <string>/Local/Default</string>
70
+ </array>
71
+ <key>dsAttrTypeStandard:SearchPolicy</key>
72
+ <string>dsAttrTypeStandard:NSPSearchPath</string>
73
+ <key>notify_of_changes</key>
74
+ <true/>
75
+ <key>requiredNodes</key>
76
+ <array>
77
+ <string>/Local/Default</string>
78
+ </array>
79
+ </dict>
80
+ <key>uuid</key>
81
+ <string>A840FC81-A6CD-4665-899E-F8B52B1C6EC4</string>
82
+ </dict>
83
+ </array>
84
+ </dict>
85
+ <key>node name</key>
86
+ <string>/Search</string>
87
+ </dict>
88
+ </plist>
89
+ CONFIG
90
+
91
+ @legacy = <<-CONFIG
92
+ <?xml version="1.0" encoding="UTF-8"?>
93
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
94
+ <plist version="1.0">
95
+ <dict>
96
+ <key>DHCP LDAP</key>
97
+ <dict>
98
+ <key>/Sets/2A7FEC83-9B78-4BD1-A4E3-29F685EFD97A</key>
99
+ <false/>
100
+ </dict>
101
+ <key>Search Node PlugIn Version</key>
102
+ <string>Search Node PlugIn Version 1.7</string>
103
+ <key>Search Policy</key>
104
+ <integer>1</integer>
105
+ </dict>
106
+ </plist>
107
+ CONFIG
108
+
109
+ @sandbox = <<-SANDBOX
110
+ ;; Copyright (c) 2011 Apple Inc. All Rights reserved.
111
+ ;;
112
+ ;; WARNING: The sandbox rules in this file currently constitute
113
+ ;; Apple System Private Interface and are subject to change at any time and
114
+ ;; without notice.
115
+ ;;
116
+
117
+ (version 1)
118
+
119
+ (allow default)
120
+
121
+ ;; deny lookup to coreservicesd to avoid deadlocks
122
+ (deny mach-lookup
123
+ (global-name "com.apple.CoreServices.coreservicesd")
124
+ (with no-log)
125
+ (with no-callout)
126
+ )
127
+
128
+ ;; we don't allow inbound or bound ports
129
+ (deny network-inbound (with no-callout))
130
+ (deny network-bind (with no-callout))
131
+
132
+ (allow network-inbound (local udp))
133
+ (allow network-bind (local ip))
134
+
135
+ ;; deny all file writes except those explicity allowed
136
+ (deny file-write* (with no-callout))
137
+
138
+ (allow process-exec (literal "/usr/bin/nsupdate"))
139
+
140
+ ;; allow slapconfig to be launched without sandbox due to denial of file-write
141
+ (allow process-exec (literal "/usr/sbin/slapconfig") (with no-sandbox))
142
+
143
+ ;; 10735867
144
+ (allow process-exec (literal "/usr/sbin/kextcache") (with no-sandbox))
145
+
146
+ ;; restrict where we write
147
+ (allow file-write*
148
+ ;; key OpenDirectory files
149
+ (mount-relative-regex
150
+ ;; our database and shadowhash files
151
+ ;; we might be targeting '/' or some other volume
152
+ #"^(/private)?/var/db/dslocal/nodes/Default(/|$)"
153
+ #"^(/private)?/var/db/dslocal/nodes/Earth(/|$)"
154
+ #"^(/private)?/var/db/dslocal/nodes/MCX(/|$)"
155
+ #"^(/private)?/var/db/shadow(/|$)"
156
+ )
157
+
158
+ (regex
159
+ ;; configuration files
160
+ ;; we ignore DirectoryService files because they are handled by dspluginhelperd
161
+ #"^/Library/Preferences/OpenDirectory"
162
+
163
+ ;; our log files
164
+ #"^(/private)?/var/log/opendirectoryd.log"
165
+
166
+ ;; configuration files
167
+ #"^/Library/Preferences/SystemConfiguration/"
168
+ )
169
+
170
+ ;; our SystemCache related files
171
+ (regex
172
+ #"^(/private)?/etc/memberd.conf" ;;; for unlinking
173
+ )
174
+
175
+ ;; update system keychain
176
+ (regex
177
+ #"^/Library/Keychains/System.keychain"
178
+ #"^/Library/Keychains/\."
179
+ #"^(/private)?/var/db/mds/system/mds.lock$"
180
+ )
181
+
182
+ ;; kerberos keytab updates
183
+ (regex
184
+ #"^(/private)?/etc/krb5.keytab$"
185
+ #"^(/private)?/tmp/krb5cc_"
186
+ )
187
+
188
+ ;; additional required
189
+ (literal "/dev/dtracehelper")
190
+ (literal "/dev/null")
191
+ (literal "/dev/random")
192
+
193
+ ;; for NIS support
194
+ (regex
195
+ #"^(/private)?/var/yp/"
196
+ #"^(/private)?/etc/defaultdomain$"
197
+ )
198
+ )
199
+
200
+ SANDBOX
201
+
202
+ File.open(MacAdmin::DSLocalNode::PREFERENCES, 'w') { |f| f.write(@config) }
203
+ File.open(MacAdmin::DSLocalNode::PREFERENCES_LEGACY, 'w') { |f| f.write(@legacy) }
204
+
205
+ end
206
+
207
+ before :each do
208
+ File.open(MacAdmin::DSLocalNode::SANDBOX_FILE, 'w') { |f| f.write(@sandbox) }
209
+ end
210
+
211
+ after :each do
212
+ if subject.name.eql? 'Earth'
213
+ FileUtils.rm_rf subject.root if File.exists? subject.root
214
+ FileUtils.rm_rf MacAdmin::DSLocalNode::SANDBOX_FILE if File.exists? MacAdmin::DSLocalNode::SANDBOX_FILE
215
+ end
216
+ end
217
+
218
+ describe '#new' do
219
+
220
+ context 'when intialized without arguments it returns the Default node' do
221
+ subject { DSLocalNode.new }
222
+ it { should be_an_instance_of DSLocalNode }
223
+ its (:name) { should eq 'Default' }
224
+ its (:label) { should eq '/Local/Default' }
225
+ its (:root) { should eq "#{MacAdmin::DSLocalNode::DSLOCAL_ROOT}/Default" }
226
+ end
227
+
228
+ context 'when intialized with a name (String)' do
229
+ subject { DSLocalNode.new 'Earth' }
230
+ it { should be_an_instance_of DSLocalNode }
231
+ its (:name) { should eq 'Earth' }
232
+ its (:label) { should eq '/Local/Earth' }
233
+ its (:root) { should eq "#{MacAdmin::DSLocalNode::DSLOCAL_ROOT}/Earth" }
234
+ end
235
+
236
+ end
237
+
238
+ describe '#exists?' do
239
+
240
+ context 'when the defined directory structure exists' do
241
+ subject { DSLocalNode.new }
242
+ it { subject.exists?.should be_true }
243
+ end
244
+
245
+ context 'when the defined directory structure does NOT exist' do
246
+ subject { DSLocalNode.new 'Earth' }
247
+ it { subject.exists?.should be_false }
248
+ end
249
+
250
+ end
251
+
252
+ describe '#active?' do
253
+
254
+ context "when the node is in the Directory Service's search path" do
255
+ subject { DSLocalNode.new }
256
+ it { subject.active?.should be_true }
257
+ end
258
+
259
+ context "when the node is NOT in the Directory Service's search path" do
260
+ subject { DSLocalNode.new 'Earth' }
261
+ it { subject.active?.should be_false }
262
+ end
263
+
264
+ end
265
+
266
+ describe '#create' do
267
+
268
+ subject { DSLocalNode.new 'Earth' }
269
+ it 'creates the defined directory structure' do
270
+ subject.create.should be_true
271
+ subject.exists?.should be_true
272
+ end
273
+
274
+ end
275
+
276
+ describe '#activate' do
277
+
278
+ subject { DSLocalNode.new 'Earth' }
279
+ it "add the node to Directory Service's search path" do
280
+ subject.activate.should be_true
281
+ subject.active?.should be_true
282
+ end
283
+
284
+ end
285
+
286
+ describe '#deactivate' do
287
+
288
+ subject { DSLocalNode.new 'Earth' }
289
+ it "removes the node from the Directory Service's search path" do
290
+ subject.activate
291
+ subject.deactivate.should be_true
292
+ subject.active?.should be_false
293
+ end
294
+
295
+ end
296
+
297
+ describe "#exists_and_active?" do
298
+
299
+ context "when the node exists and is active" do
300
+ subject { DSLocalNode.new }
301
+ it { subject.exists_and_active?.should be_true }
302
+ end
303
+
304
+ context "when the node exists but is NOT active" do
305
+ subject { DSLocalNode.new 'Earth' }
306
+ it 'should be_false' do
307
+ subject.create
308
+ subject.exists_and_active?.should be_false
309
+ end
310
+ end
311
+
312
+ context "when the node is active but does NOT exist" do
313
+ subject { DSLocalNode.new 'Earth' }
314
+ it 'should be_false' do
315
+ subject.activate
316
+ subject.exists_and_active?.should be_false
317
+ end
318
+ end
319
+
320
+ end
321
+
322
+ describe "#create_and_activate" do
323
+ subject { DSLocalNode.new 'Earth' }
324
+ it "should be_true" do
325
+ subject.create_and_activate
326
+ subject.exists_and_active?.should be_true
327
+ end
328
+ end
329
+
330
+ describe "#destroy_and_deactivate" do
331
+ subject { DSLocalNode.new 'Earth' }
332
+ it "should be_false" do
333
+ subject.create_and_activate
334
+ subject.destroy_and_deactivate
335
+ subject.exists_and_active?.should be_false
336
+ end
337
+ end
338
+
339
+ after :all do
340
+ FileUtils.rm_rf @test_dir
341
+ end
342
+
343
+ end
@@ -0,0 +1,275 @@
1
+ require 'spec_helper'
2
+
3
+ describe MacAdmin::Group do
4
+
5
+ before :all do
6
+ # Create a dslocal sandbox
7
+ @test_dir = "/private/tmp/macadmin_group_test.#{rand(100000)}"
8
+ MacAdmin::DSLocalRecord.send(:remove_const, :DSLOCAL_ROOT)
9
+ MacAdmin::DSLocalRecord::DSLOCAL_ROOT = File.expand_path @test_dir
10
+ FileUtils.mkdir_p "#{@test_dir}/Default/groups"
11
+ FileUtils.mkdir_p "#{@test_dir}/Default/users"
12
+
13
+ # Create two group record plists that we can load
14
+ foo = {
15
+ "gid" => ["501"],
16
+ "realname" => ["Foo"],
17
+ "name" => ["foo"],
18
+ "generateduid" => ["00000000-0000-0000-0000-000000000001"],
19
+ }
20
+
21
+ bar = {
22
+ "gid" => ["502"],
23
+ "realname" => ["Bar"],
24
+ "name" => ["bar"],
25
+ "generateduid" => ["00000000-0000-0000-0000-000000000002"],
26
+ }
27
+
28
+ [foo, bar].each do |group|
29
+ record = CFPropertyList::List.new
30
+ record.value = CFPropertyList.guess(group)
31
+ record.save("#{@test_dir}/Default/groups/#{group['name'].first}.plist", CFPropertyList::List::FORMAT_BINARY)
32
+ end
33
+ end
34
+
35
+ describe '#new' do
36
+
37
+ context 'throws an ArgumentError when given fewer than 1 params' do
38
+ subject { Group.new }
39
+ it { expect { subject }.to raise_error(ArgumentError) }
40
+ end
41
+
42
+ context 'when created from a parameter Hash' do
43
+ subject { Group.new :name => 'baz', :gid => "666" }
44
+ it { should be_an_instance_of Group }
45
+ it 'should have a valid generateduid attribute' do
46
+ (UUID.valid?(subject.generateduid.first)).should be_true
47
+ end
48
+ its (:gid) { should eq ['666'] }
49
+ its (:name) { should eq ['baz'] }
50
+ its (:realname) { should eq ['baz'.capitalize] }
51
+ its (:users) { should eq [] }
52
+ its (:groupmembers) { should eq [] }
53
+ end
54
+
55
+ context 'when created from a just a name (String)' do
56
+ subject { Group.new 'baz' }
57
+ it { should be_an_instance_of Group }
58
+ it 'should have a valid generateduid attribute' do
59
+ (UUID.valid?(subject.generateduid.first)).should be_true
60
+ end
61
+ its (:gid) { should eq ['503'] } # 503 should be the next available GID
62
+ its (:name) { should eq ['baz'] }
63
+ its (:realname) { should eq ['baz'.capitalize] }
64
+ its (:users) { should eq [] }
65
+ its (:groupmembers) { should eq [] }
66
+ end
67
+
68
+ end
69
+
70
+ describe '#exists?' do
71
+
72
+ context "when the object is different from its associated file" do
73
+ subject { Group.new :name => 'foo', :gid => "503" }
74
+ it 'should return false' do
75
+ subject.exists?.should be_false
76
+ end
77
+ end
78
+
79
+ context "when ALL the object's attributes match its associated file" do
80
+ subject { Group.new :name => 'foo', :gid => "501" }
81
+ it 'should return true' do
82
+ subject.exists?.should be_true
83
+ end
84
+ end
85
+
86
+ context "when the object does not have an associated file on disk" do
87
+ subject { Group.new :name => 'baz' }
88
+ it 'should return false' do
89
+ subject.exists?.should be_false
90
+ end
91
+ end
92
+
93
+ end
94
+
95
+ describe '#create' do
96
+
97
+ context "with NO path parameter (default)" do
98
+ subject { Group.new :name => 'baz', :gid => "666" }
99
+ it 'saves the record to disk on the derived path' do
100
+ subject.create.should be_true
101
+ File.exists?(subject.file).should be_true
102
+ end
103
+ after do
104
+ FileUtils.rm_rf subject.file
105
+ end
106
+ end
107
+
108
+ context "with a path parameter" do
109
+ path = "/private/tmp/group-create-method-test.plist"
110
+ subject { Group.new :name => 'baz', :gid => "666" }
111
+ it 'saves the record to disk on the path specified' do
112
+ subject.create(path).should be_true
113
+ end
114
+ after do
115
+ FileUtils.rm_rf path
116
+ end
117
+ end
118
+
119
+ end
120
+
121
+ describe '#destroy' do
122
+ subject { Group.new :name => 'baz', :gid => "666" }
123
+ it 'removes the record on disk and returns true' do
124
+ subject.create
125
+ subject.destroy.should be_true
126
+ File.exists?(subject.file).should be_false
127
+ end
128
+ end
129
+
130
+ describe '#users=' do
131
+ subject { Group.new 'foo' }
132
+ before { subject.users = ['foo', 'bar'] }
133
+ it 'replaces the membership array for users' do
134
+ subject['users'].should eq ['foo', 'bar']
135
+ end
136
+ end
137
+
138
+ describe '#users' do
139
+ subject { Group.new 'foo' }
140
+ before { subject.users = ['foo', 'bar'] }
141
+ it 'returns a membership array of users' do
142
+ subject.users.should eq ['foo', 'bar']
143
+ end
144
+ end
145
+
146
+ describe '#has_user?' do
147
+ subject { Group.new 'baz' }
148
+
149
+ context 'when membership array is non-existent' do
150
+ it 'should be false' do
151
+ subject.has_user?('bender').should be_false
152
+ end
153
+ end
154
+
155
+ context 'when membership array is populated' do
156
+ before { subject.users = ['fry', 'bender'] }
157
+ it 'should be true if the user is a member of the group' do
158
+ subject.has_user?('fry').should be_true
159
+ end
160
+
161
+ it 'should be false if the user is NOT a member of the group' do
162
+ subject.has_user?('leela').should be_false
163
+ end
164
+ end
165
+
166
+ end
167
+
168
+ describe '#add_user' do
169
+ subject { Group.new 'baz' }
170
+
171
+ context 'when the member does NOT exist' do
172
+ it 'should raise an error' do
173
+ expect { subject.add_user('invalid') }.to raise_error(StandardError)
174
+ end
175
+ end
176
+
177
+ context 'when the member exists' do
178
+ it "should add the user to the users array" do
179
+ fry = User.new 'fry'
180
+ fry.create
181
+ subject.add_user('fry').should be_nil
182
+ subject['users'].should eql ["fry"]
183
+ fry.destroy
184
+ end
185
+ end
186
+
187
+ end
188
+
189
+ describe '#rm_user' do
190
+ subject { Group.new 'foo' }
191
+
192
+ context 'when user does NOT exist' do
193
+ it 'should return nil' do
194
+ subject.users = ['fry', 'bender']
195
+ subject.rm_user('leela').should be_nil
196
+ end
197
+ end
198
+
199
+ context 'when user does exist' do
200
+ it 'should remove the user from the membership array' do
201
+ subject.users = ['fry', 'bender']
202
+ subject.rm_user('fry').should eq 'fry'
203
+ subject.users.should eq ['bender']
204
+ end
205
+ end
206
+
207
+ end
208
+
209
+ describe '#has_groupmember?' do
210
+ subject { Group.new 'baz' }
211
+
212
+ context 'when membership array is non-existent' do
213
+ it 'should be false' do
214
+ subject.has_groupmember?('foo').should be_false
215
+ end
216
+ end
217
+
218
+ context 'when membership array is populated' do
219
+ it 'should be true if the user is a member of the group' do
220
+ subject.groupmembers = ['00000000-0000-0000-0000-000000000001']
221
+ subject.has_groupmember?('foo').should be_true
222
+ end
223
+
224
+ it 'should be false if the user is NOT a member of the group' do
225
+ subject.has_groupmember?('leela').should be_false
226
+ end
227
+ end
228
+
229
+ end
230
+
231
+ describe '#add_groupmember' do
232
+ subject { Group.new 'baz' }
233
+
234
+ context 'when the member does NOT exist' do
235
+ it 'should raise an error' do
236
+ expect { subject.add_groupmember('invalid') }.to raise_error(StandardError)
237
+ end
238
+ end
239
+
240
+ context 'when the member exists' do
241
+ it "should add named group's GeneratedUID to the groupmembers array" do
242
+ subject.add_groupmember('foo').should be_nil
243
+ subject['groupmembers'].should eql ["00000000-0000-0000-0000-000000000001"]
244
+ end
245
+ end
246
+
247
+ end
248
+
249
+ describe '#rm_groupmember' do
250
+ subject { Group.new 'baz' }
251
+
252
+ context 'when the member does NOT exist' do
253
+ it 'should return nil' do
254
+ subject.rm_groupmember('invalid').should be_nil
255
+ end
256
+ end
257
+
258
+ context 'when the member exists' do
259
+ it 'should remove the member from the array' do
260
+ ['foo', 'bar'].each do |member|
261
+ subject.add_groupmember member
262
+ end
263
+ subject.rm_groupmember('foo').should eq "00000000-0000-0000-0000-000000000001"
264
+ subject.groupmembers.should eq ["00000000-0000-0000-0000-000000000002"]
265
+ end
266
+ end
267
+
268
+ end
269
+
270
+
271
+ after :all do
272
+ FileUtils.rm_rf @test_dir
273
+ end
274
+
275
+ end