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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +163 -0
- data/Rakefile +23 -0
- data/ext/macadmin/password/crypto.c +89 -0
- data/ext/macadmin/password/extconf.rb +3 -0
- data/lib/macadmin.rb +22 -0
- data/lib/macadmin/common.rb +80 -0
- data/lib/macadmin/dslocal.rb +252 -0
- data/lib/macadmin/dslocal/computer.rb +22 -0
- data/lib/macadmin/dslocal/computergroup.rb +19 -0
- data/lib/macadmin/dslocal/dslocalnode.rb +281 -0
- data/lib/macadmin/dslocal/group.rb +82 -0
- data/lib/macadmin/dslocal/user.rb +113 -0
- data/lib/macadmin/mcx.rb +227 -0
- data/lib/macadmin/password.rb +72 -0
- data/lib/macadmin/shadowhash.rb +297 -0
- data/lib/macadmin/version.rb +8 -0
- data/macadmin.gemspec +35 -0
- data/spec/common_spec.rb +49 -0
- data/spec/computer_spec.rb +133 -0
- data/spec/computergroup_spec.rb +274 -0
- data/spec/dslocal_spec.rb +179 -0
- data/spec/dslocalnode_spec.rb +343 -0
- data/spec/group_spec.rb +275 -0
- data/spec/macadmin_spec.rb +13 -0
- data/spec/mcx_spec.rb +145 -0
- data/spec/password_spec.rb +40 -0
- data/spec/shadowhash_spec.rb +309 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/user_spec.rb +248 -0
- metadata +218 -0
@@ -0,0 +1,274 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MacAdmin::ComputerGroup do
|
4
|
+
|
5
|
+
before :all do
|
6
|
+
# Create a dslocal sandbox
|
7
|
+
@test_dir = "/private/tmp/macadmin_computergroup_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/computergroups"
|
11
|
+
FileUtils.mkdir_p "#{@test_dir}/Default/computers"
|
12
|
+
|
13
|
+
# Create two group record plists that we can load
|
14
|
+
fratbots = {
|
15
|
+
"gid" => ["1010"],
|
16
|
+
"realname" => ["Fratbots"],
|
17
|
+
"name" => ["fratbots"],
|
18
|
+
"generateduid" => ["00000000-0000-0000-0000-000000000001"],
|
19
|
+
}
|
20
|
+
|
21
|
+
robot_mafia = {
|
22
|
+
"gid" => ["502"],
|
23
|
+
"realname" => ["Robot Mafia"],
|
24
|
+
"name" => ["robot_mafia"],
|
25
|
+
"generateduid" => ["00000000-0000-0000-0000-000000000002"],
|
26
|
+
}
|
27
|
+
|
28
|
+
[fratbots, robot_mafia].each do |computergroup|
|
29
|
+
record = CFPropertyList::List.new
|
30
|
+
record.value = CFPropertyList.guess(computergroup)
|
31
|
+
record.save("#{@test_dir}/Default/computergroups/#{computergroup['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 { ComputerGroup.new }
|
39
|
+
it { expect { subject }.to raise_error(ArgumentError) }
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'when created from a parameter Hash' do
|
43
|
+
subject { ComputerGroup.new :name => 'league-of-robots', :gid => "666" }
|
44
|
+
it { should be_an_instance_of ComputerGroup }
|
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 ['league-of-robots'] }
|
50
|
+
its (:realname) { should eq ['league-of-robots'.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 { ComputerGroup.new 'league-of-robots' }
|
57
|
+
it { should be_an_instance_of ComputerGroup }
|
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 ['league-of-robots'] }
|
63
|
+
its (:realname) { should eq ['league-of-robots'.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 { ComputerGroup.new :name => 'fratbots', :gid => "1111" }
|
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 { ComputerGroup.new :name => 'fratbots', :gid => "1010" }
|
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 { ComputerGroup.new :name => 'league-of-robots' }
|
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 { ComputerGroup.new :name => 'league-of-robots', :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 { ComputerGroup.new :name => 'league-of-robots', :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 { ComputerGroup.new :name => 'league-of-robots', :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 { ComputerGroup.new 'fratbots' }
|
132
|
+
before { subject.users = ['fratbots', 'robot_mafia'] }
|
133
|
+
it 'replaces the membership array for users' do
|
134
|
+
subject['users'].should eq ['fratbots', 'robot_mafia']
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe '#users' do
|
139
|
+
subject { ComputerGroup.new 'fratbots' }
|
140
|
+
before { subject.users = ['fratbots', 'robot_mafia'] }
|
141
|
+
it 'returns a membership array of users' do
|
142
|
+
subject.users.should eq ['fratbots', 'robot_mafia']
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
describe '#has_user?' do
|
147
|
+
subject { ComputerGroup.new 'league-of-robots' }
|
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 = ['bender', 'crushinator'] }
|
157
|
+
it 'should be true if the user is a member of the group' do
|
158
|
+
subject.has_user?('crushinator').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?('flexo').should be_false
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
167
|
+
|
168
|
+
describe '#add_user' do
|
169
|
+
subject { ComputerGroup.new 'league-of-robots' }
|
170
|
+
|
171
|
+
context 'when the member does NOT exist' do
|
172
|
+
it 'should raise an error' do
|
173
|
+
expect { subject.add_user('flexo') }.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
|
+
crushinator = Computer.new 'crushinator'
|
180
|
+
crushinator.create
|
181
|
+
subject.add_user('crushinator').should be_nil
|
182
|
+
subject['users'].should eql ["crushinator"]
|
183
|
+
crushinator.destroy
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
describe '#rm_user' do
|
190
|
+
subject { ComputerGroup.new 'fratbots' }
|
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 { ComputerGroup.new 'league-of-robots' }
|
211
|
+
|
212
|
+
context 'when membership array is non-existent' do
|
213
|
+
it 'should be false' do
|
214
|
+
subject.has_groupmember?('fratbots').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?('fratbots').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?('killbots').should be_false
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
230
|
+
|
231
|
+
describe '#add_groupmember' do
|
232
|
+
subject { ComputerGroup.new 'league-of-robots' }
|
233
|
+
|
234
|
+
context 'when the member does NOT exist' do
|
235
|
+
it 'should raise an error' do
|
236
|
+
expect { subject.add_groupmember('killbots') }.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('fratbots').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 { ComputerGroup.new 'league-of-robots' }
|
251
|
+
|
252
|
+
context 'when the member does NOT exist' do
|
253
|
+
it 'should return nil' do
|
254
|
+
subject.rm_groupmember('killbots').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
|
+
['fratbots', 'robot_mafia'].each do |member|
|
261
|
+
subject.add_groupmember member
|
262
|
+
end
|
263
|
+
subject.rm_groupmember('fratbots').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
|
+
after :all do
|
271
|
+
FileUtils.rm_rf @test_dir
|
272
|
+
end
|
273
|
+
|
274
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MacAdmin::DSLocalRecord do
|
4
|
+
|
5
|
+
before :all do
|
6
|
+
# Create a dslocal sandbox
|
7
|
+
@test_dir = "/private/tmp/macadmin_dslocal_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 a user record plist that we can load
|
14
|
+
fry = {
|
15
|
+
"uid" => ["501"],
|
16
|
+
"name" => ["fry"],
|
17
|
+
"realname" => ["Phillip J. Fry"],
|
18
|
+
"generateduid" => ["00000000-0000-0000-0000-000000000001"],
|
19
|
+
}
|
20
|
+
|
21
|
+
record = CFPropertyList::List.new
|
22
|
+
record.value = CFPropertyList.guess(fry)
|
23
|
+
record.save("#{@test_dir}/Default/users/#{fry['name'].first}.plist", CFPropertyList::List::FORMAT_BINARY)
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#init_with_file' do
|
27
|
+
before do
|
28
|
+
@user = <<-RECORD
|
29
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
30
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
31
|
+
<plist version="1.0">
|
32
|
+
<dict>
|
33
|
+
<key>generateduid</key>
|
34
|
+
<array>
|
35
|
+
<string>331D5FCC-DBE1-4193-9DBF-BC955E997B3E</string>
|
36
|
+
</array>
|
37
|
+
<key>name</key>
|
38
|
+
<array>
|
39
|
+
<string>foo</string>
|
40
|
+
</array>
|
41
|
+
<key>uid</key>
|
42
|
+
<array>
|
43
|
+
<string>411</string>
|
44
|
+
</array>
|
45
|
+
<key>gid</key>
|
46
|
+
<array>
|
47
|
+
<string>80</string>
|
48
|
+
</array>
|
49
|
+
<key>home</key>
|
50
|
+
<array>
|
51
|
+
<string>/Users/foo</string>
|
52
|
+
</array>
|
53
|
+
<key>shell</key>
|
54
|
+
<array>
|
55
|
+
<string>/bin/bash</string>
|
56
|
+
</array>
|
57
|
+
<key>realname</key>
|
58
|
+
<array>
|
59
|
+
<string>foo</string>
|
60
|
+
</array>
|
61
|
+
</dict>
|
62
|
+
</plist>
|
63
|
+
RECORD
|
64
|
+
@test_dir = "/private/tmp/test.#{rand(100000)}"
|
65
|
+
@file = "#{@test_dir}/Default/users/foo.plist"
|
66
|
+
FileUtils.mkdir_p "#{@test_dir}/Default/users"
|
67
|
+
File.open(@file, mode='w') { |f| f.write @user }
|
68
|
+
end
|
69
|
+
|
70
|
+
subject { User.init_with_file @file }
|
71
|
+
it { should be_an_instance_of User }
|
72
|
+
its (:uid) { should eq ['411'] }
|
73
|
+
its (:gid) { should eq ['80'] }
|
74
|
+
its (:name) { should eq ['foo'] }
|
75
|
+
its (:home) { should eq ["/Users/foo"] }
|
76
|
+
its (:shell) { should eq ["/bin/bash"] }
|
77
|
+
its (:realname) { should eq ['foo'] }
|
78
|
+
its (:generateduid) { should eq ['331D5FCC-DBE1-4193-9DBF-BC955E997B3E'] }
|
79
|
+
|
80
|
+
after do
|
81
|
+
FileUtils.rm_rf @test_dir
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
describe '#new' do
|
87
|
+
|
88
|
+
context 'throws an ArgumentError when given fewer than 1 params' do
|
89
|
+
subject { DSLocalRecord.new }
|
90
|
+
it { expect { subject }.to raise_error(ArgumentError) }
|
91
|
+
end
|
92
|
+
|
93
|
+
context 'throws a DSLocalError when given an invalid name attribute' do
|
94
|
+
subject { DSLocalRecord.new 'bad name' }
|
95
|
+
it { expect { subject }.to raise_error(DSLocalError) }
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'throws a DSLocalError when given an invalid name attribute' do
|
99
|
+
subject { DSLocalRecord.new 'fry' }
|
100
|
+
it 'takes a String argument and returns a DSLocalRecord object' do
|
101
|
+
subject.should be_an_instance_of DSLocalRecord
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context 'throws a DSLocalError when given an invalid name attribute' do
|
106
|
+
subject { DSLocalRecord.new :name => 'fry', :gid => 80, :uid => 666 }
|
107
|
+
it 'takes a Hash argument and returns a DSLocalRecord object' do
|
108
|
+
subject.should be_an_instance_of DSLocalRecord
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
describe '#exists?' do
|
115
|
+
|
116
|
+
context "when the object is different from its associated file" do
|
117
|
+
subject { DSLocalRecord.new :name => 'fry', :uid => 666 }
|
118
|
+
it { subject.exists?.should be_false }
|
119
|
+
end
|
120
|
+
|
121
|
+
context "when the object is identical to its associated file" do
|
122
|
+
subject { DSLocalRecord.new :name => 'fry',
|
123
|
+
:uid => "501",
|
124
|
+
:generateduid => "00000000-0000-0000-0000-000000000001",
|
125
|
+
:file => @test_dir + '/Default/users/fry.plist' }
|
126
|
+
it { subject.exists?.should be_true }
|
127
|
+
end
|
128
|
+
|
129
|
+
context "when the object does not have an associated file on disk" do
|
130
|
+
subject { DSLocalRecord.new 'bender' }
|
131
|
+
it { subject.exists?.should be_false }
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
describe '#create' do
|
137
|
+
|
138
|
+
context "with NO path parameter (default)" do
|
139
|
+
subject { DSLocalRecord.new :name => 'bender', :uid => "666", :file => @test_dir + '/Default/users/bender.plist' }
|
140
|
+
it 'saves the record to disk on the derived path' do
|
141
|
+
subject.create.should be_true
|
142
|
+
File.exists?(subject.file).should be_true
|
143
|
+
end
|
144
|
+
after do
|
145
|
+
FileUtils.rm_rf subject.file
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
context "with a path parameter" do
|
150
|
+
mode = 33152
|
151
|
+
path = "/private/tmp/group-create-method-test.plist"
|
152
|
+
subject { DSLocalRecord.new :name => 'leela', :uid => "609" }
|
153
|
+
it 'saves the record to disk on the path specified' do
|
154
|
+
subject.create(path).should be_true
|
155
|
+
stat = File::Stat.new path
|
156
|
+
File.exists?(path).should be_true
|
157
|
+
stat.mode.should eq mode
|
158
|
+
end
|
159
|
+
after do
|
160
|
+
FileUtils.rm_rf path
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
describe '#destroy' do
|
167
|
+
subject { DSLocalRecord.new :name => 'zoidberg', :file => @test_dir + '/Default/users/bender.plist' }
|
168
|
+
it 'removes the record on disk and returns true' do
|
169
|
+
subject.create
|
170
|
+
subject.destroy.should be_true
|
171
|
+
File.exists?(subject.file).should be_false
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
after :all do
|
176
|
+
FileUtils.rm_rf @test_dir
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|