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,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